From 26ab54b6e5d554c767fa5b58d0263ca828a1e24a Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Tue, 28 Mar 2017 17:14:31 +0200 Subject: [PATCH] update rackspace/opencloud to 1.16.0 Signed-off-by: Robin Appelman --- .gitignore | 3 + composer.json | 2 +- composer.lock | 270 ++--- composer/autoload_classmap.php | 575 +++++++++-- composer/autoload_namespaces.php | 8 +- composer/autoload_psr4.php | 1 + composer/autoload_static.php | 600 +++++++++-- composer/installed.json | 452 ++++----- guzzle/guzzle/.gitignore | 27 + guzzle/guzzle/CHANGELOG.md | 762 ++++++++++++++ guzzle/guzzle/LICENSE | 19 + guzzle/guzzle/README.md | 212 ++++ guzzle/guzzle/UPGRADING.md | 537 ++++++++++ guzzle/guzzle/build.xml | 45 + guzzle/guzzle/composer.json | 74 ++ guzzle/guzzle/docs/Makefile | 153 +++ .../docs/_downloads/guzzle-schema-1.0.json | 176 ++++ guzzle/guzzle/docs/_static/guzzle-icon.png | Bin 0 -> 803 bytes guzzle/guzzle/docs/_static/homepage.css | 122 +++ guzzle/guzzle/docs/_static/logo.png | Bin 0 -> 248750 bytes guzzle/guzzle/docs/_static/prettify.css | 41 + guzzle/guzzle/docs/_static/prettify.js | 28 + guzzle/guzzle/docs/_templates/index.html | 103 ++ guzzle/guzzle/docs/_templates/leftbar.html | 0 guzzle/guzzle/docs/_templates/nav_links.html | 5 + guzzle/guzzle/docs/batching/batching.rst | 183 ++++ guzzle/guzzle/docs/conf.py | 95 ++ guzzle/guzzle/docs/docs.rst | 73 ++ guzzle/guzzle/docs/getting-started/faq.rst | 29 + .../docs/getting-started/installation.rst | 154 +++ .../guzzle/docs/getting-started/overview.rst | 85 ++ guzzle/guzzle/docs/http-client/client.rst | 569 +++++++++++ .../guzzle/docs/http-client/entity-bodies.rst | 151 +++ .../docs/http-client/http-redirects.rst | 99 ++ guzzle/guzzle/docs/http-client/request.rst | 667 ++++++++++++ guzzle/guzzle/docs/http-client/response.rst | 141 +++ .../guzzle/docs/http-client/uri-templates.rst | 52 + guzzle/guzzle/docs/index.rst | 5 + .../docs/iterators/guzzle-iterators.rst | 97 ++ .../docs/iterators/resource-iterators.rst | 149 +++ guzzle/guzzle/docs/plugins/async-plugin.rst | 18 + guzzle/guzzle/docs/plugins/backoff-plugin.rst | 22 + guzzle/guzzle/docs/plugins/cache-plugin.rst | 169 ++++ guzzle/guzzle/docs/plugins/cookie-plugin.rst | 33 + .../guzzle/docs/plugins/creating-plugins.rst | 93 ++ .../guzzle/docs/plugins/curl-auth-plugin.rst | 32 + guzzle/guzzle/docs/plugins/history-plugin.rst | 24 + guzzle/guzzle/docs/plugins/log-plugin.rst | 69 ++ .../docs/plugins/md5-validator-plugin.rst | 29 + guzzle/guzzle/docs/plugins/mock-plugin.rst | 27 + guzzle/guzzle/docs/plugins/oauth-plugin.rst | 30 + .../guzzle/docs/plugins/plugins-list.rst.inc | 9 + .../guzzle/docs/plugins/plugins-overview.rst | 59 ++ guzzle/guzzle/docs/requirements.txt | 2 + guzzle/guzzle/docs/testing/unit-testing.rst | 201 ++++ .../guzzle-service-descriptions.rst | 619 ++++++++++++ .../using-the-service-builder.rst | 316 ++++++ .../webservice-client/webservice-client.rst | 659 ++++++++++++ guzzle/guzzle/phar-stub.php | 16 + guzzle/guzzle/phing/build.properties.dist | 16 + guzzle/guzzle/phing/imports/dependencies.xml | 33 + guzzle/guzzle/phing/imports/deploy.xml | 142 +++ .../guzzle/phing/tasks/ComposerLintTask.php | 152 +++ .../phing/tasks/GuzzlePearPharPackageTask.php | 338 +++++++ .../guzzle/phing/tasks/GuzzleSubSplitTask.php | 385 +++++++ guzzle/guzzle/phpunit.xml.dist | 48 + .../Guzzle/Batch/AbstractBatchDecorator.php | 66 ++ guzzle/guzzle/src/Guzzle/Batch/Batch.php | 92 ++ .../guzzle/src/Guzzle/Batch/BatchBuilder.php | 199 ++++ .../src/Guzzle/Batch/BatchClosureDivisor.php | 39 + .../src/Guzzle/Batch/BatchClosureTransfer.php | 40 + .../src/Guzzle/Batch/BatchCommandTransfer.php | 75 ++ .../Guzzle/Batch/BatchDivisorInterface.php | 18 + .../src/Guzzle/Batch/BatchInterface.php | 32 + .../src/Guzzle/Batch/BatchRequestTransfer.php | 65 ++ .../src/Guzzle/Batch/BatchSizeDivisor.php | 47 + .../Guzzle/Batch/BatchTransferInterface.php | 16 + .../Exception/BatchTransferException.php | 90 ++ .../Guzzle/Batch/ExceptionBufferingBatch.php | 50 + .../guzzle/src/Guzzle/Batch/FlushingBatch.php | 60 ++ .../guzzle/src/Guzzle/Batch/HistoryBatch.php | 39 + .../src/Guzzle/Batch/NotifyingBatch.php | 38 + guzzle/guzzle/src/Guzzle/Batch/composer.json | 31 + .../src/Guzzle/Cache/AbstractCacheAdapter.php | 21 + .../src/Guzzle/Cache/CacheAdapterFactory.php | 117 +++ .../Guzzle/Cache/CacheAdapterInterface.php | 55 + .../src/Guzzle/Cache/ClosureCacheAdapter.php | 57 ++ .../src/Guzzle/Cache/DoctrineCacheAdapter.php | 41 + .../src/Guzzle/Cache/NullCacheAdapter.php | 31 + .../src/Guzzle/Cache/Zf1CacheAdapter.php | 44 + .../src/Guzzle/Cache/Zf2CacheAdapter.php | 41 + guzzle/guzzle/src/Guzzle/Cache/composer.json | 27 + .../Guzzle/Common/AbstractHasDispatcher.php | 0 .../src}/Guzzle/Common/Collection.php | 0 .../src}/Guzzle/Common/Event.php | 0 .../Exception/BadMethodCallException.php | 0 .../Common/Exception/ExceptionCollection.php | 0 .../Common/Exception/GuzzleException.php | 0 .../Exception/InvalidArgumentException.php | 0 .../Common/Exception/RuntimeException.php | 0 .../Exception/UnexpectedValueException.php | 0 .../Guzzle/Common/FromConfigInterface.php | 0 .../Guzzle/Common/HasDispatcherInterface.php | 0 .../src}/Guzzle/Common/ToArrayInterface.php | 0 .../src}/Guzzle/Common/Version.php | 0 .../src}/Guzzle/Common/composer.json | 0 .../Http/AbstractEntityBodyDecorator.php | 0 .../src}/Guzzle/Http/CachingEntityBody.php | 0 .../src}/Guzzle/Http/Client.php | 0 .../src}/Guzzle/Http/ClientInterface.php | 0 .../src}/Guzzle/Http/Curl/CurlHandle.php | 0 .../src}/Guzzle/Http/Curl/CurlMulti.php | 0 .../Guzzle/Http/Curl/CurlMultiInterface.php | 0 .../src}/Guzzle/Http/Curl/CurlMultiProxy.php | 0 .../src}/Guzzle/Http/Curl/CurlVersion.php | 0 .../src}/Guzzle/Http/Curl/RequestMediator.php | 0 .../src}/Guzzle/Http/EntityBody.php | 0 .../src}/Guzzle/Http/EntityBodyInterface.php | 0 .../Http/Exception/BadResponseException.php | 0 .../ClientErrorResponseException.php | 0 .../CouldNotRewindStreamException.php | 0 .../Guzzle/Http/Exception/CurlException.php | 0 .../Guzzle/Http/Exception/HttpException.php | 0 .../Http/Exception/MultiTransferException.php | 0 .../Http/Exception/RequestException.php | 0 .../ServerErrorResponseException.php | 0 .../Exception/TooManyRedirectsException.php | 0 .../src}/Guzzle/Http/IoEmittingEntityBody.php | 0 .../Guzzle/Http/Message/AbstractMessage.php | 0 .../Http/Message/EntityEnclosingRequest.php | 0 .../EntityEnclosingRequestInterface.php | 0 .../src}/Guzzle/Http/Message/Header.php | 0 .../Http/Message/Header/CacheControl.php | 0 .../Http/Message/Header/HeaderCollection.php | 0 .../Http/Message/Header/HeaderFactory.php | 0 .../Message/Header/HeaderFactoryInterface.php | 0 .../Http/Message/Header/HeaderInterface.php | 0 .../src}/Guzzle/Http/Message/Header/Link.php | 0 .../Guzzle/Http/Message/MessageInterface.php | 0 .../src}/Guzzle/Http/Message/PostFile.php | 0 .../Guzzle/Http/Message/PostFileInterface.php | 0 .../src}/Guzzle/Http/Message/Request.php | 0 .../Guzzle/Http/Message/RequestFactory.php | 0 .../Http/Message/RequestFactoryInterface.php | 0 .../Guzzle/Http/Message/RequestInterface.php | 0 .../src}/Guzzle/Http/Message/Response.php | 0 .../src}/Guzzle/Http/Mimetypes.php | 0 .../Http/QueryAggregator/CommaAggregator.php | 0 .../QueryAggregator/DuplicateAggregator.php | 0 .../Http/QueryAggregator/PhpAggregator.php | 0 .../QueryAggregatorInterface.php | 0 .../src}/Guzzle/Http/QueryString.php | 0 .../src}/Guzzle/Http/ReadLimitEntityBody.php | 0 .../src}/Guzzle/Http/RedirectPlugin.php | 0 .../src}/Guzzle/Http/Resources/cacert.pem | 0 .../src}/Guzzle/Http/Resources/cacert.pem.md5 | 0 .../src}/Guzzle/Http/StaticClient.php | 0 .../{http => guzzle/src}/Guzzle/Http/Url.php | 0 .../src}/Guzzle/Http/composer.json | 0 .../src/Guzzle/Inflection/Inflector.php | 38 + .../Guzzle/Inflection/InflectorInterface.php | 27 + .../Guzzle/Inflection/MemoizingInflector.php | 70 ++ .../Inflection/PreComputedInflector.php | 59 ++ .../src/Guzzle/Inflection/composer.json | 26 + .../src/Guzzle/Iterator/AppendIterator.php | 19 + .../src/Guzzle/Iterator/ChunkedIterator.php | 56 ++ .../src/Guzzle/Iterator/FilterIterator.php | 36 + .../src/Guzzle/Iterator/MapIterator.php | 34 + .../Guzzle/Iterator/MethodProxyIterator.php | 27 + guzzle/guzzle/src/Guzzle/Iterator/README.md | 25 + .../guzzle/src/Guzzle/Iterator/composer.json | 27 + .../src/Guzzle/Log/AbstractLogAdapter.php | 16 + .../guzzle/src/Guzzle/Log/ArrayLogAdapter.php | 34 + .../src/Guzzle/Log/ClosureLogAdapter.php | 23 + .../src/Guzzle/Log/LogAdapterInterface.php | 18 + .../src/Guzzle/Log/MessageFormatter.php | 179 ++++ .../src/Guzzle/Log/MonologLogAdapter.php | 34 + .../guzzle/src/Guzzle/Log/PsrLogAdapter.php | 36 + .../guzzle/src/Guzzle/Log/Zf1LogAdapter.php | 24 + .../guzzle/src/Guzzle/Log/Zf2LogAdapter.php | 21 + guzzle/guzzle/src/Guzzle/Log/composer.json | 29 + .../Guzzle/Parser/Cookie/CookieParser.php | 0 .../Parser/Cookie/CookieParserInterface.php | 0 .../Parser/Message/AbstractMessageParser.php | 0 .../Guzzle/Parser/Message/MessageParser.php | 0 .../Parser/Message/MessageParserInterface.php | 0 .../Parser/Message/PeclHttpMessageParser.php | 0 .../src}/Guzzle/Parser/ParserRegistry.php | 0 .../Parser/UriTemplate/PeclUriTemplate.php | 0 .../Guzzle/Parser/UriTemplate/UriTemplate.php | 0 .../UriTemplate/UriTemplateInterface.php | 0 .../src}/Guzzle/Parser/Url/UrlParser.php | 0 .../Guzzle/Parser/Url/UrlParserInterface.php | 0 .../src}/Guzzle/Parser/composer.json | 0 .../src/Guzzle/Plugin/Async/AsyncPlugin.php | 84 ++ .../src/Guzzle/Plugin/Async/composer.json | 27 + .../Backoff/AbstractBackoffStrategy.php | 91 ++ .../AbstractErrorCodeBackoffStrategy.php | 40 + .../Guzzle/Plugin/Backoff/BackoffLogger.php | 76 ++ .../Guzzle/Plugin/Backoff/BackoffPlugin.php | 126 +++ .../Backoff/BackoffStrategyInterface.php | 30 + .../Backoff/CallbackBackoffStrategy.php | 47 + .../Backoff/ConstantBackoffStrategy.php | 34 + .../Plugin/Backoff/CurlBackoffStrategy.php | 28 + .../Backoff/ExponentialBackoffStrategy.php | 25 + .../Plugin/Backoff/HttpBackoffStrategy.php | 30 + .../Plugin/Backoff/LinearBackoffStrategy.php | 36 + .../Backoff/ReasonPhraseBackoffStrategy.php | 25 + .../Backoff/TruncatedBackoffStrategy.php | 36 + .../src/Guzzle/Plugin/Backoff/composer.json | 28 + .../Cache/CacheKeyProviderInterface.php | 11 + .../src/Guzzle/Plugin/Cache/CachePlugin.php | 353 +++++++ .../Plugin/Cache/CacheStorageInterface.php | 43 + .../Plugin/Cache/CallbackCanCacheStrategy.php | 53 + .../Cache/CanCacheStrategyInterface.php | 30 + .../Plugin/Cache/DefaultCacheKeyProvider.php | 46 + .../Plugin/Cache/DefaultCacheStorage.php | 251 +++++ .../Plugin/Cache/DefaultCanCacheStrategy.php | 32 + .../Plugin/Cache/DefaultRevalidation.php | 172 ++++ .../Guzzle/Plugin/Cache/DenyRevalidation.php | 19 + .../Plugin/Cache/RevalidationInterface.php | 32 + .../Guzzle/Plugin/Cache/SkipRevalidation.php | 19 + .../src/Guzzle/Plugin/Cache/composer.json | 28 + .../src/Guzzle/Plugin/Cookie/Cookie.php | 513 ++++++++++ .../Cookie/CookieJar/ArrayCookieJar.php | 222 ++++ .../Cookie/CookieJar/CookieJarInterface.php | 85 ++ .../Plugin/Cookie/CookieJar/FileCookieJar.php | 65 ++ .../src/Guzzle/Plugin/Cookie/CookiePlugin.php | 70 ++ .../Exception/InvalidCookieException.php | 7 + .../src/Guzzle/Plugin/Cookie/composer.json | 27 + .../Guzzle/Plugin/CurlAuth/CurlAuthPlugin.php | 46 + .../src/Guzzle/Plugin/CurlAuth/composer.json | 27 + .../ErrorResponseExceptionInterface.php | 22 + .../ErrorResponse/ErrorResponsePlugin.php | 72 ++ .../Exception/ErrorResponseException.php | 7 + .../Guzzle/Plugin/ErrorResponse/composer.json | 27 + .../Guzzle/Plugin/History/HistoryPlugin.php | 163 +++ .../src/Guzzle/Plugin/History/composer.json | 27 + .../src/Guzzle/Plugin/Log/LogPlugin.php | 161 +++ .../src/Guzzle/Plugin/Log/composer.json | 28 + .../Plugin/Md5/CommandContentMd5Plugin.php | 57 ++ .../Guzzle/Plugin/Md5/Md5ValidatorPlugin.php | 88 ++ .../src/Guzzle/Plugin/Md5/composer.json | 27 + .../src/Guzzle/Plugin/Mock/MockPlugin.php | 242 +++++ .../src/Guzzle/Plugin/Mock/composer.json | 27 + .../src/Guzzle/Plugin/Oauth/OauthPlugin.php | 305 ++++++ .../src/Guzzle/Plugin/Oauth/composer.json | 27 + guzzle/guzzle/src/Guzzle/Plugin/composer.json | 44 + .../Guzzle/Service/AbstractConfigLoader.php | 177 ++++ .../Guzzle/Service/Builder/ServiceBuilder.php | 189 ++++ .../Builder/ServiceBuilderInterface.php | 40 + .../Service/Builder/ServiceBuilderLoader.php | 89 ++ .../Guzzle/Service/CachingConfigLoader.php | 46 + guzzle/guzzle/src/Guzzle/Service/Client.php | 293 ++++++ .../src/Guzzle/Service/ClientInterface.php | 68 ++ .../Service/Command/AbstractCommand.php | 390 ++++++++ .../Guzzle/Service/Command/ClosureCommand.php | 41 + .../Service/Command/CommandInterface.php | 128 +++ .../Command/CreateResponseClassEvent.php | 32 + .../Command/DefaultRequestSerializer.php | 169 ++++ .../Service/Command/DefaultResponseParser.php | 55 + .../Service/Command/Factory/AliasFactory.php | 39 + .../Command/Factory/CompositeFactory.php | 154 +++ .../Command/Factory/ConcreteClassFactory.php | 47 + .../Command/Factory/FactoryInterface.php | 21 + .../Service/Command/Factory/MapFactory.php | 27 + .../Factory/ServiceDescriptionFactory.php | 71 ++ .../Request/AbstractRequestVisitor.php | 69 ++ .../LocationVisitor/Request/BodyVisitor.php | 58 ++ .../LocationVisitor/Request/HeaderVisitor.php | 44 + .../LocationVisitor/Request/JsonVisitor.php | 63 ++ .../Request/PostFieldVisitor.php | 18 + .../Request/PostFileVisitor.php | 24 + .../LocationVisitor/Request/QueryVisitor.php | 18 + .../Request/RequestVisitorInterface.php | 31 + .../Request/ResponseBodyVisitor.php | 18 + .../LocationVisitor/Request/XmlVisitor.php | 252 +++++ .../Response/AbstractResponseVisitor.php | 26 + .../LocationVisitor/Response/BodyVisitor.php | 23 + .../Response/HeaderVisitor.php | 50 + .../LocationVisitor/Response/JsonVisitor.php | 93 ++ .../Response/ReasonPhraseVisitor.php | 23 + .../Response/ResponseVisitorInterface.php | 46 + .../Response/StatusCodeVisitor.php | 23 + .../LocationVisitor/Response/XmlVisitor.php | 149 +++ .../LocationVisitor/VisitorFlyweight.php | 138 +++ .../Service/Command/OperationCommand.php | 89 ++ .../Command/OperationResponseParser.php | 195 ++++ .../Command/RequestSerializerInterface.php | 21 + .../Command/ResponseClassInterface.php | 18 + .../Command/ResponseParserInterface.php | 18 + .../Guzzle/Service/ConfigLoaderInterface.php | 22 + .../Guzzle/Service/Description/Operation.php | 547 ++++++++++ .../Description/OperationInterface.php | 159 +++ .../Guzzle/Service/Description/Parameter.php | 925 +++++++++++++++++ .../Service/Description/SchemaFormatter.php | 156 +++ .../Service/Description/SchemaValidator.php | 290 ++++++ .../Description/ServiceDescription.php | 271 +++++ .../ServiceDescriptionInterface.php | 106 ++ .../Description/ServiceDescriptionLoader.php | 64 ++ .../Description/ValidatorInterface.php | 28 + .../Service/Exception/CommandException.php | 7 + .../Exception/CommandTransferException.php | 119 +++ .../Exception/DescriptionBuilderException.php | 7 + .../InconsistentClientTransferException.php | 38 + .../Exception/ResponseClassException.php | 9 + .../Exception/ServiceBuilderException.php | 7 + .../Exception/ServiceNotFoundException.php | 5 + .../Service/Exception/ValidationException.php | 30 + .../AbstractResourceIteratorFactory.php | 37 + .../CompositeResourceIteratorFactory.php | 67 ++ .../Resource/MapResourceIteratorFactory.php | 34 + .../src/Guzzle/Service/Resource/Model.php | 64 ++ .../Service/Resource/ResourceIterator.php | 254 +++++ .../Resource/ResourceIteratorApplyBatched.php | 111 ++ .../Resource/ResourceIteratorClassFactory.php | 60 ++ .../ResourceIteratorFactoryInterface.php | 30 + .../Resource/ResourceIteratorInterface.php | 61 ++ .../guzzle/src/Guzzle/Service/composer.json | 29 + .../Guzzle/Stream/PhpStreamRequestFactory.php | 0 .../src}/Guzzle/Stream/Stream.php | 0 .../src}/Guzzle/Stream/StreamInterface.php | 0 .../Stream/StreamRequestFactoryInterface.php | 0 .../src}/Guzzle/Stream/composer.json | 0 .../Batch/AbstractBatchDecoratorTest.php | 33 + .../Guzzle/Tests/Batch/BatchBuilderTest.php | 86 ++ .../Tests/Batch/BatchClosureDivisorTest.php | 36 + .../Tests/Batch/BatchClosureTransferTest.php | 52 + .../Tests/Batch/BatchCommandTransferTest.php | 83 ++ .../Tests/Batch/BatchRequestTransferTest.php | 80 ++ .../Tests/Batch/BatchSizeDivisorTest.php | 24 + .../tests/Guzzle/Tests/Batch/BatchTest.php | 91 ++ .../Batch/ExceptionBufferingBatchTest.php | 45 + .../Guzzle/Tests/Batch/FlushingBatchTest.php | 40 + .../Guzzle/Tests/Batch/HistoryBatchTest.php | 26 + .../Guzzle/Tests/Batch/NotifyingBatchTest.php | 45 + .../Tests/Cache/CacheAdapterFactoryTest.php | 64 ++ .../Guzzle/Tests/Cache/CacheAdapterTest.php | 68 ++ .../Tests/Cache/ClosureCacheAdapterTest.php | 94 ++ .../Tests/Cache/NullCacheAdapterTest.php | 20 + .../Tests/Cache/Zf2CacheAdapterTest.php | 58 ++ .../Common/AbstractHasDispatcherTest.php | 63 ++ .../Guzzle/Tests/Common/CollectionTest.php | 529 ++++++++++ .../tests/Guzzle/Tests/Common/EventTest.php | 62 ++ .../Exception/BatchTransferExceptionTest.php | 21 + .../Exception/ExceptionCollectionTest.php | 66 ++ .../tests/Guzzle/Tests/Common/VersionTest.php | 27 + .../tests/Guzzle/Tests/GuzzleTestCase.php | 235 +++++ .../Http/AbstractEntityBodyDecoratorTest.php | 34 + .../Tests/Http/CachingEntityBodyTest.php | 249 +++++ .../tests/Guzzle/Tests/Http/ClientTest.php | 592 +++++++++++ .../Guzzle/Tests/Http/Curl/CurlHandleTest.php | 945 ++++++++++++++++++ .../Tests/Http/Curl/CurlMultiProxyTest.php | 100 ++ .../Guzzle/Tests/Http/Curl/CurlMultiTest.php | 455 +++++++++ .../Tests/Http/Curl/CurlVersionTest.php | 39 + .../Tests/Http/Curl/RequestMediatorTest.php | 67 ++ .../Guzzle/Tests/Http/EntityBodyTest.php | 182 ++++ .../Http/Exception/CurlExceptionTest.php | 27 + .../Tests/Http/Exception/ExceptionTest.php | 66 ++ .../Exception/MultiTransferExceptionTest.php | 51 + .../Tests/Http/IoEmittingEntityBodyTest.php | 47 + .../Http/Message/AbstractMessageTest.php | 136 +++ .../Message/EntityEnclosingRequestTest.php | 434 ++++++++ .../Http/Message/Header/HeaderFactoryTest.php | 29 + .../Tests/Http/Message/Header/LinkTest.php | 63 ++ .../Tests/Http/Message/HeaderComparison.php | 135 +++ .../Http/Message/HeaderComparisonTest.php | 115 +++ .../Guzzle/Tests/Http/Message/HeaderTest.php | 162 +++ .../Tests/Http/Message/PostFileTest.php | 88 ++ .../Tests/Http/Message/RequestFactoryTest.php | 613 ++++++++++++ .../Guzzle/Tests/Http/Message/RequestTest.php | 625 ++++++++++++ .../Tests/Http/Message/ResponseTest.php | 677 +++++++++++++ .../tests/Guzzle/Tests/Http/MimetypesTest.php | 31 + .../QueryAggregator/CommaAggregatorTest.php | 30 + .../DuplicateAggregatorTest.php | 30 + .../QueryAggregator/PhpAggregatorTest.php | 32 + .../Guzzle/Tests/Http/QueryStringTest.php | 232 +++++ .../Tests/Http/ReadLimitEntityBodyTest.php | 81 ++ .../Guzzle/Tests/Http/RedirectPluginTest.php | 277 +++++ .../guzzle/tests/Guzzle/Tests/Http/Server.php | 187 ++++ .../Guzzle/Tests/Http/StaticClientTest.php | 67 ++ .../tests/Guzzle/Tests/Http/UrlTest.php | 298 ++++++ .../guzzle/tests/Guzzle/Tests/Http/server.js | 146 +++ .../Guzzle/Tests/Inflection/InflectorTest.php | 37 + .../Inflection/MemoizingInflectorTest.php | 46 + .../Inflection/PreComputedInflectorTest.php | 45 + .../Tests/Iterator/AppendIteratorTest.php | 29 + .../Tests/Iterator/ChunkedIteratorTest.php | 60 ++ .../Tests/Iterator/FilterIteratorTest.php | 28 + .../Guzzle/Tests/Iterator/MapIteratorTest.php | 28 + .../Iterator/MethodProxyIteratorTest.php | 28 + .../Guzzle/Tests/Log/ArrayLogAdapterTest.php | 23 + .../Tests/Log/ClosureLogAdapterTest.php | 30 + .../Guzzle/Tests/Log/MessageFormatterTest.php | 143 +++ .../Guzzle/Tests/Log/PsrLogAdapterTest.php | 25 + .../Guzzle/Tests/Log/Zf2LogAdapterTest.php | 51 + .../Guzzle/Tests/Mock/CustomResponseModel.php | 21 + .../Guzzle/Tests/Mock/ErrorResponseMock.php | 25 + .../tests/Guzzle/Tests/Mock/ExceptionMock.php | 11 + .../tests/Guzzle/Tests/Mock/MockMulti.php | 11 + .../tests/Guzzle/Tests/Mock/MockObserver.php | 65 ++ .../tests/Guzzle/Tests/Mock/MockSubject.php | 7 + .../Parser/Cookie/CookieParserProvider.php | 297 ++++++ .../Tests/Parser/Cookie/CookieParserTest.php | 22 + .../Parser/Message/MessageParserProvider.php | 225 +++++ .../Parser/Message/MessageParserTest.php | 58 ++ .../Message/PeclHttpMessageParserTest.php | 36 + .../Tests/Parser/ParserRegistryTest.php | 33 + .../UriTemplate/AbstractUriTemplateTest.php | 113 +++ .../UriTemplate/PeclUriTemplateTest.php | 27 + .../Parser/UriTemplate/UriTemplateTest.php | 106 ++ .../Tests/Plugin/Async/AsyncPluginTest.php | 93 ++ .../Backoff/AbstractBackoffStrategyTest.php | 86 ++ .../Plugin/Backoff/BackoffLoggerTest.php | 110 ++ .../Plugin/Backoff/BackoffPluginTest.php | 297 ++++++ .../Backoff/CallbackBackoffStrategyTest.php | 31 + .../Backoff/ConstantBackoffStrategyTest.php | 20 + .../Backoff/CurlBackoffStrategyTest.php | 36 + .../ExponentialBackoffStrategyTest.php | 23 + .../Backoff/HttpBackoffStrategyTest.php | 47 + .../Backoff/LinearBackoffStrategyTest.php | 21 + .../ReasonPhraseBackoffStrategyTest.php | 32 + .../Backoff/TruncatedBackoffStrategyTest.php | 30 + .../Tests/Plugin/Cache/CachePluginTest.php | 441 ++++++++ .../Cache/CallbackCanCacheStrategyTest.php | 72 ++ .../Plugin/Cache/DefaultCacheStorageTest.php | 193 ++++ .../Cache/DefaultCanCacheStrategyTest.php | 40 + .../Plugin/Cache/DefaultRevalidationTest.php | 246 +++++ .../Plugin/Cache/DenyRevalidationTest.php | 19 + .../Plugin/Cache/SkipRevalidationTest.php | 19 + .../Cookie/CookieJar/ArrayCookieJarTest.php | 353 +++++++ .../Cookie/CookieJar/FileCookieJarTest.php | 63 ++ .../Tests/Plugin/Cookie/CookiePluginTest.php | 134 +++ .../Guzzle/Tests/Plugin/Cookie/CookieTest.php | 208 ++++ .../Plugin/CurlAuth/CurlAuthPluginTest.php | 39 + .../ErrorResponse/ErrorResponsePluginTest.php | 137 +++ .../Plugin/History/HistoryPluginTest.php | 140 +++ .../Guzzle/Tests/Plugin/Log/LogPluginTest.php | 95 ++ .../Md5/CommandContentMd5PluginTest.php | 97 ++ .../Plugin/Md5/Md5ValidatorPluginTest.php | 120 +++ .../Tests/Plugin/Mock/MockPluginTest.php | 198 ++++ .../Tests/Plugin/Oauth/OauthPluginTest.php | 329 ++++++ .../Service/AbstractConfigLoaderTest.php | 149 +++ .../Builder/ServiceBuilderLoaderTest.php | 177 ++++ .../Service/Builder/ServiceBuilderTest.php | 317 ++++++ .../Tests/Service/CachingConfigLoaderTest.php | 43 + .../tests/Guzzle/Tests/Service/ClientTest.php | 307 ++++++ .../Service/Command/AbstractCommandTest.php | 16 + .../Service/Command/ClosureCommandTest.php | 54 + .../Tests/Service/Command/CommandTest.php | 437 ++++++++ .../Command/DefaultRequestSerializerTest.php | 122 +++ .../Command/DefaultResponseParserTest.php | 59 ++ .../Command/Factory/AliasFactoryTest.php | 76 ++ .../Command/Factory/CompositeFactoryTest.php | 124 +++ .../Factory/ConcreteClassFactoryTest.php | 49 + .../Command/Factory/MapFactoryTest.php | 37 + .../Factory/ServiceDescriptionFactoryTest.php | 68 ++ .../Request/AbstractVisitorTestCase.php | 110 ++ .../Request/BodyVisitorTest.php | 63 ++ .../Request/HeaderVisitorTest.php | 48 + .../Request/JsonVisitorTest.php | 60 ++ .../Request/PostFieldVisitorTest.php | 33 + .../Request/PostFileVisitorTest.php | 54 + .../Request/QueryVisitorTest.php | 48 + .../Request/ResponseBodyVisitorTest.php | 20 + .../Request/XmlVisitorTest.php | 558 +++++++++++ .../Response/AbstractResponseVisitorTest.php | 29 + .../Response/BodyVisitorTest.php | 21 + .../Response/HeaderVisitorTest.php | 98 ++ .../Response/JsonVisitorTest.php | 157 +++ .../Response/ReasonPhraseVisitorTest.php | 21 + .../Response/StatusCodeVisitorTest.php | 21 + .../Response/XmlVisitorTest.php | 415 ++++++++ .../LocationVisitor/VisitorFlyweightTest.php | 53 + .../Service/Command/OperationCommandTest.php | 102 ++ .../Command/OperationResponseParserTest.php | 335 +++++++ .../Service/Description/OperationTest.php | 308 ++++++ .../Service/Description/ParameterTest.php | 411 ++++++++ .../Description/SchemaFormatterTest.php | 61 ++ .../Description/SchemaValidatorTest.php | 326 ++++++ .../ServiceDescriptionLoaderTest.php | 177 ++++ .../Description/ServiceDescriptionTest.php | 240 +++++ .../CommandTransferExceptionTest.php | 66 ++ ...nconsistentClientTransferExceptionTest.php | 15 + .../Exception/ValidationExceptionTest.php | 17 + .../Service/Mock/Command/IterableCommand.php | 31 + .../Service/Mock/Command/MockCommand.php | 32 + .../Service/Mock/Command/OtherCommand.php | 30 + .../Tests/Service/Mock/Command/Sub/Sub.php | 7 + .../Guzzle/Tests/Service/Mock/MockClient.php | 36 + .../Mock/Model/MockCommandIterator.php | 42 + .../CompositeResourceIteratorFactoryTest.php | 37 + .../MapResourceIteratorFactoryTest.php | 40 + .../Tests/Service/Resource/ModelTest.php | 65 ++ .../ResourceIteratorClassFactoryTest.php | 41 + .../Service/Resource/ResourceIteratorTest.php | 185 ++++ .../Stream/PhpStreamRequestFactoryTest.php | 172 ++++ .../tests/Guzzle/Tests/Stream/StreamTest.php | 189 ++++ .../tests/Guzzle/Tests/TestData/FileBody.txt | 0 .../Tests/TestData/description/bar.json | 3 + .../Tests/TestData/description/baz.json | 3 + .../Tests/TestData/description/foo.json | 8 + .../Tests/TestData/description/recursive.json | 3 + .../tests/Guzzle/Tests/TestData/mock_response | 3 + .../Guzzle/Tests/TestData/services/json1.json | 18 + .../Guzzle/Tests/TestData/services/json2.json | 11 + .../Tests/TestData/services/services.json | 71 ++ .../Guzzle/Tests/TestData/test_service.json | 40 + .../Guzzle/Tests/TestData/test_service2.json | 7 + guzzle/guzzle/tests/bootstrap.php | 10 + mikemccabe/json-patch-php/.gitignore | 5 + mikemccabe/json-patch-php/.gitmodules | 3 + mikemccabe/json-patch-php/README.md | 79 ++ mikemccabe/json-patch-php/composer.json | 11 + mikemccabe/json-patch-php/local_tests.json | 219 ++++ mikemccabe/json-patch-php/run_tests.php | 217 ++++ .../json-patch-php/simplexml_tests.json | 65 ++ mikemccabe/json-patch-php/src/JsonPatch.php | 600 +++++++++++ rackspace/php-opencloud/CODE_OF_CONDUCT.md | 50 + rackspace/php-opencloud/CONTRIBUTING.md | 43 +- rackspace/php-opencloud/README.md | 17 +- rackspace/php-opencloud/apigen.neon | 3 + rackspace/php-opencloud/composer.json | 30 +- .../php-opencloud/docs/changelog/1.7.0.md | 2 +- rackspace/php-opencloud/docs/generate.sh | 52 +- .../docs/getting-started-openstack.md | 189 +--- .../php-opencloud/docs/getting-started.md | 177 +--- .../docs/userguide/Autoscale/Config.md | 46 +- .../docs/userguide/Autoscale/Groups.md | 95 +- .../docs/userguide/Autoscale/Policies.md | 67 +- .../docs/userguide/Autoscale/Webhooks.md | 51 +- .../php-opencloud/docs/userguide/Clients.md | 98 +- .../docs/userguide/CloudMonitoring/Agents.md | 148 +-- .../docs/userguide/CloudMonitoring/Alarms.md | 55 +- .../userguide/CloudMonitoring/Changelogs.md | 13 +- .../docs/userguide/CloudMonitoring/Checks.md | 140 +-- .../userguide/CloudMonitoring/Entities.md | 54 +- .../docs/userguide/CloudMonitoring/Metrics.md | 78 +- .../CloudMonitoring/Notifications.md | 202 +--- .../docs/userguide/CloudMonitoring/Service.md | 15 +- .../docs/userguide/CloudMonitoring/Views.md | 18 +- .../docs/userguide/CloudMonitoring/Zones.md | 45 +- .../docs/userguide/Compute/Images.md | 64 +- .../docs/userguide/Compute/Keypair.md | 83 +- .../docs/userguide/Compute/Server.md | 142 +-- .../docs/userguide/Compute/Service.md | 17 +- .../docs/userguide/Identity/Roles.md | 67 +- .../docs/userguide/Identity/Service.md | 23 +- .../docs/userguide/Identity/Tenants.md | 20 +- .../docs/userguide/Identity/Tokens.md | 83 +- .../docs/userguide/Identity/Users.md | 127 +-- .../php-opencloud/docs/userguide/Iterators.md | 117 +-- .../docs/userguide/LoadBalancer/README.md | 79 +- .../docs/userguide/LoadBalancer/USERGUIDE.md | 306 +----- .../docs/userguide/ObjectStore/Access.md | 65 +- .../docs/userguide/ObjectStore/Account.md | 27 +- .../userguide/ObjectStore/CDN/Container.md | 68 +- .../docs/userguide/ObjectStore/CDN/Object.md | 19 +- .../ObjectStore/Storage/Container.md | 180 +--- .../ObjectStore/Storage/Migrating.md | 82 +- .../userguide/ObjectStore/Storage/Object.md | 262 +---- .../docs/userguide/Queues/Claim.md | 122 +-- .../docs/userguide/Queues/Message.md | 218 +--- .../docs/userguide/Queues/Queue.md | 155 +-- .../php-opencloud/docs/userguide/accessip.md | 50 +- .../docs/userguide/caching-credentials.md | 38 +- .../php-opencloud/docs/userguide/dbaas.md | 184 +--- .../php-opencloud/docs/userguide/flavors.md | 70 +- .../php-opencloud/docs/userguide/networks.md | 121 +-- .../php-opencloud/docs/userguide/servers.md | 179 +--- .../php-opencloud/docs/userguide/volumes.md | 192 +--- .../Autoscale/Resource/AbstractResource.php | 60 +- .../OpenCloud/Autoscale/Resource/Group.php | 104 +- .../Autoscale/Resource/GroupConfiguration.php | 31 +- .../Resource/LaunchConfiguration.php | 40 +- .../Autoscale/Resource/ScalingPolicy.php | 55 +- .../OpenCloud/Autoscale/Resource/Webhook.php | 35 +- .../lib/OpenCloud/Autoscale/Service.php | 32 +- .../lib/OpenCloud/CDN/Resource/Flavor.php | 51 + .../lib/OpenCloud/CDN/Resource/Service.php | 115 +++ .../lib/OpenCloud/CDN/Service.php | 188 ++++ .../Collection/MonitoringIterator.php | 35 + .../Exception/AgentException.php | 21 +- .../Exception/AlarmException.php | 21 +- .../Exception/CheckException.php | 21 +- .../Exception/CloudMonitoringException.php | 21 +- .../Exception/EntityException.php | 21 +- .../Exception/MetricException.php | 21 +- .../NotificationHistoryException.php | 21 +- .../Exception/NotificationPlanException.php | 21 +- .../Exception/ServiceException.php | 21 +- .../Exception/TestException.php | 21 +- .../Exception/ZoneException.php | 21 +- .../Resource/AbstractResource.php | 40 +- .../CloudMonitoring/Resource/Agent.php | 73 +- .../Resource/AgentConnection.php | 22 +- .../CloudMonitoring/Resource/AgentHost.php | 31 +- .../Resource/AgentHostInfo.php | 21 +- .../CloudMonitoring/Resource/AgentTarget.php | 29 +- .../CloudMonitoring/Resource/AgentToken.php | 25 +- .../CloudMonitoring/Resource/Alarm.php | 37 +- .../CloudMonitoring/Resource/Changelog.php | 24 +- .../CloudMonitoring/Resource/Check.php | 29 +- .../CloudMonitoring/Resource/CheckType.php | 22 +- .../CloudMonitoring/Resource/Entity.php | 22 +- .../CloudMonitoring/Resource/Metric.php | 21 +- .../Resource/MetricDataPoint.php | 22 + .../CloudMonitoring/Resource/Notification.php | 32 +- .../Resource/NotificationHistory.php | 29 +- .../Resource/NotificationPlan.php | 26 +- .../Resource/NotificationType.php | 24 +- .../Resource/ReadOnlyResource.php | 45 +- .../CloudMonitoring/Resource/View.php | 28 +- .../CloudMonitoring/Resource/Zone.php | 32 +- .../lib/OpenCloud/CloudMonitoring/Service.php | 25 +- .../lib/OpenCloud/Common/ArrayAccess.php | 60 ++ .../lib/OpenCloud/Common/Base.php | 146 +-- .../lib/OpenCloud/Common/Collection.php | 57 +- .../Common/Collection/ArrayCollection.php | 70 +- .../Common/Collection/PaginatedIterator.php | 65 +- .../Common/Collection/ResourceIterator.php | 38 +- .../OpenCloud/Common/Constants/Datetime.php | 39 +- .../lib/OpenCloud/Common/Constants/Header.php | 113 ++- .../lib/OpenCloud/Common/Constants/Mime.php | 24 +- .../OpenCloud/Common/Constants/Service.php | 29 +- .../lib/OpenCloud/Common/Constants/Size.php | 28 +- .../lib/OpenCloud/Common/Constants/State.php | 27 +- .../Common/Exceptions/AsyncError.php | 19 +- .../Common/Exceptions/AsyncHttpError.php | 19 +- .../Common/Exceptions/AsyncTimeoutError.php | 19 +- .../Common/Exceptions/AttributeError.php | 19 +- .../Common/Exceptions/AuthenticationError.php | 19 +- .../Common/Exceptions/BackupInstanceError.php | 22 + .../Common/Exceptions/BackupNameError.php | 22 + .../Common/Exceptions/BaseException.php | 17 +- .../OpenCloud/Common/Exceptions/CdnError.php | 19 +- .../Common/Exceptions/CdnHttpError.php | 19 +- .../Exceptions/CdnNotAvailableError.php | 19 +- .../Common/Exceptions/CdnTtlError.php | 19 +- .../Common/Exceptions/CollectionException.php | 19 +- .../Exceptions/ContainerCreateError.php | 19 +- .../Exceptions/ContainerDeleteError.php | 19 +- .../Common/Exceptions/ContainerError.php | 19 +- .../Common/Exceptions/ContainerNameError.php | 19 +- .../Exceptions/ContainerNotEmptyError.php | 19 +- .../Exceptions/ContainerNotFoundError.php | 19 +- .../Common/Exceptions/CreateError.php | 19 +- .../Common/Exceptions/CreateUpdateError.php | 19 +- .../Common/Exceptions/CredentialError.php | 19 +- .../Common/Exceptions/DatabaseCreateError.php | 19 +- .../Common/Exceptions/DatabaseDeleteError.php | 19 +- .../Common/Exceptions/DatabaseListError.php | 19 +- .../Common/Exceptions/DatabaseNameError.php | 19 +- .../Common/Exceptions/DatabaseUpdateError.php | 19 +- .../Common/Exceptions/DeleteError.php | 19 +- .../Common/Exceptions/DocumentError.php | 19 +- .../Common/Exceptions/DomainError.php | 19 +- .../Exceptions/DomainNotFoundException.php | 22 + .../Common/Exceptions/EmptyResponseError.php | 19 +- .../Common/Exceptions/EndpointError.php | 19 +- .../Common/Exceptions/FlavorError.php | 19 +- .../ForbiddenOperationException.php | 25 + .../OpenCloud/Common/Exceptions/HttpError.php | 19 +- .../Common/Exceptions/HttpForbiddenError.php | 19 +- .../Common/Exceptions/HttpOverLimitError.php | 19 +- .../Exceptions/HttpResponseException.php | 56 ++ .../Common/Exceptions/HttpRetryError.php | 19 +- .../Common/Exceptions/HttpTimeoutError.php | 19 +- .../Exceptions/HttpUnauthorizedError.php | 19 +- .../Common/Exceptions/HttpUrlError.php | 19 +- .../OpenCloud/Common/Exceptions/IOError.php | 19 +- .../Common/Exceptions/IdRequiredError.php | 19 +- .../Common/Exceptions/ImageError.php | 19 +- .../Common/Exceptions/InstanceCreateError.php | 19 +- .../Common/Exceptions/InstanceDeleteError.php | 19 +- .../Common/Exceptions/InstanceError.php | 19 +- .../Common/Exceptions/InstanceFlavorError.php | 19 +- .../Common/Exceptions/InstanceNotFound.php | 19 +- .../Common/Exceptions/InstanceUpdateError.php | 19 +- .../Exceptions/InvalidArgumentError.php | 19 +- .../Common/Exceptions/InvalidIdTypeError.php | 19 +- .../Common/Exceptions/InvalidIpTypeError.php | 19 +- .../Exceptions/InvalidParameterError.php | 19 +- .../Common/Exceptions/InvalidRequestError.php | 19 +- .../Exceptions/InvalidTemplateError.php | 22 + .../OpenCloud/Common/Exceptions/JsonError.php | 19 +- .../Common/Exceptions/LoggingException.php | 20 +- .../Common/Exceptions/MetadataCreateError.php | 19 +- .../Common/Exceptions/MetadataDeleteError.php | 19 +- .../Common/Exceptions/MetadataError.php | 19 +- .../Common/Exceptions/MetadataJsonError.php | 19 +- .../Common/Exceptions/MetadataKeyError.php | 19 +- .../Common/Exceptions/MetadataPrefixError.php | 19 +- .../Common/Exceptions/MetadataUpdateError.php | 19 +- .../Exceptions/MisMatchedChecksumError.php | 19 +- .../Common/Exceptions/MissingValueError.php | 19 +- .../OpenCloud/Common/Exceptions/NameError.php | 19 +- .../Common/Exceptions/NetworkCreateError.php | 19 +- .../Common/Exceptions/NetworkDeleteError.php | 19 +- .../Common/Exceptions/NetworkError.php | 19 +- .../Common/Exceptions/NetworkUpdateError.php | 19 +- .../Common/Exceptions/NetworkUrlError.php | 19 +- .../Common/Exceptions/NoContentTypeError.php | 19 +- .../Common/Exceptions/NoNameError.php | 19 +- .../Common/Exceptions/ObjFetchError.php | 19 +- .../Common/Exceptions/ObjectCopyError.php | 19 +- .../Common/Exceptions/ObjectError.php | 19 +- .../Common/Exceptions/RebuildError.php | 19 +- .../Common/Exceptions/RecordTypeError.php | 19 +- .../Exceptions/ResourceBucketException.php | 22 +- .../Exceptions/ResourceNotFoundException.php | 25 + .../Common/Exceptions/RuntimeException.php | 19 +- .../Common/Exceptions/ServerActionError.php | 19 +- .../Common/Exceptions/ServerCreateError.php | 19 +- .../Common/Exceptions/ServerDeleteError.php | 19 +- .../Exceptions/ServerImageScheduleError.php | 19 +- .../Common/Exceptions/ServerIpsError.php | 19 +- .../Common/Exceptions/ServerJsonError.php | 19 +- .../Common/Exceptions/ServerUpdateError.php | 19 +- .../Common/Exceptions/ServerUrlError.php | 19 +- .../Common/Exceptions/ServiceException.php | 19 +- .../Common/Exceptions/SnapshotError.php | 19 +- .../Common/Exceptions/TempUrlMethodError.php | 19 +- .../Common/Exceptions/UnknownError.php | 19 +- .../Exceptions/UnknownParameterError.php | 19 +- .../Exceptions/UnrecognizedServiceError.php | 19 +- .../Exceptions/UnsupportedExtensionError.php | 19 +- .../UnsupportedFeatureExtension.php | 19 +- .../Exceptions/UnsupportedVersionError.php | 19 +- .../Common/Exceptions/UpdateError.php | 19 +- .../OpenCloud/Common/Exceptions/UrlError.php | 19 +- .../Common/Exceptions/UserCreateError.php | 19 +- .../Common/Exceptions/UserDeleteError.php | 19 +- .../Common/Exceptions/UserListError.php | 19 +- .../Common/Exceptions/UserNameError.php | 19 +- .../Common/Exceptions/UserUpdateError.php | 19 +- .../Common/Exceptions/VolumeError.php | 19 +- .../Common/Exceptions/VolumeTypeError.php | 19 +- .../lib/OpenCloud/Common/Http/Client.php | 51 +- .../Common/Http/Message/Formatter.php | 27 +- .../Common/Http/Message/RequestSubscriber.php | 43 +- .../lib/OpenCloud/Common/Lang.php | 48 +- .../OpenCloud/Common/Log/AbstractLogger.php | 140 --- .../lib/OpenCloud/Common/Log/LogLevel.php | 38 - .../lib/OpenCloud/Common/Log/Logger.php | 109 +- .../OpenCloud/Common/Log/LoggerInterface.php | 134 --- .../lib/OpenCloud/Common/Metadata.php | 54 +- .../lib/OpenCloud/Common/PersistentObject.php | 742 +------------- .../Common/Resource/BaseResource.php | 294 ++++++ .../Common/Resource/NovaResource.php | 51 + .../Common/Resource/PersistentResource.php | 440 ++++++++ .../Common/Resource/ReadOnlyResource.php | 42 + .../Common/Service/AbstractService.php | 36 +- .../lib/OpenCloud/Common/Service/Catalog.php | 20 +- .../OpenCloud/Common/Service/CatalogItem.php | 37 +- .../Common/Service/CatalogService.php | 47 +- .../lib/OpenCloud/Common/Service/Endpoint.php | 95 +- .../OpenCloud/Common/Service/NovaService.php | 71 +- .../Common/Service/ServiceBuilder.php | 33 +- .../Common/Service/ServiceInterface.php | 17 +- .../Compute/Constants/ImageState.php | 26 +- .../OpenCloud/Compute/Constants/Network.php | 27 +- .../Compute/Constants/ServerState.php | 24 +- .../Compute/Exception/KeyPairException.php | 23 +- .../lib/OpenCloud/Compute/Resource/Flavor.php | 39 +- .../lib/OpenCloud/Compute/Resource/Image.php | 61 +- .../OpenCloud/Compute/Resource/KeyPair.php | 46 +- .../OpenCloud/Compute/Resource/Network.php | 63 +- .../lib/OpenCloud/Compute/Resource/Server.php | 488 ++++++--- .../Compute/Resource/ServerMetadata.php | 47 +- .../Compute/Resource/VolumeAttachment.php | 37 +- .../lib/OpenCloud/Compute/Service.php | 74 +- .../OpenCloud/DNS/Collection/DnsIterator.php | 18 +- .../{Object.php => AbstractResource.php} | 86 +- .../OpenCloud/DNS/Resource/AsyncResponse.php | 52 +- .../lib/OpenCloud/DNS/Resource/Domain.php | 89 +- .../DNS/Resource/HasPtrRecordsInterface.php | 12 + .../lib/OpenCloud/DNS/Resource/PtrRecord.php | 125 ++- .../lib/OpenCloud/DNS/Resource/Record.php | 29 +- .../lib/OpenCloud/DNS/Resource/Subdomain.php | 23 +- .../lib/OpenCloud/DNS/Service.php | 141 ++- .../OpenCloud/Database/Resource/Backup.php | 92 ++ .../Database/Resource/Configuration.php | 84 ++ .../OpenCloud/Database/Resource/Database.php | 74 +- .../OpenCloud/Database/Resource/Datastore.php | 86 ++ .../Database/Resource/DatastoreVersion.php | 65 ++ .../OpenCloud/Database/Resource/Instance.php | 169 +++- .../lib/OpenCloud/Database/Resource/User.php | 156 +-- .../lib/OpenCloud/Database/Service.php | 128 ++- .../lib/OpenCloud/Identity/Constants/User.php | 25 +- .../lib/OpenCloud/Identity/Resource/Role.php | 21 +- .../OpenCloud/Identity/Resource/Tenant.php | 21 +- .../lib/OpenCloud/Identity/Resource/Token.php | 19 +- .../lib/OpenCloud/Identity/Resource/User.php | 51 +- .../lib/OpenCloud/Identity/Service.php | 30 +- .../lib/OpenCloud/Image/Enum/Document.php | 25 + .../lib/OpenCloud/Image/Enum/MemberStatus.php | 25 + .../OpenCloud/Image/Enum/OperationType.php | 28 + .../lib/OpenCloud/Image/Enum/Schema.php | 31 + .../Image/Resource/AbstractSchemaResource.php | 116 +++ .../lib/OpenCloud/Image/Resource/Image.php | 226 +++++ .../Image/Resource/ImageInterface.php | 40 + .../Image/Resource/JsonPatch/Document.php | 81 ++ .../Image/Resource/JsonPatch/Encoder.php | 66 ++ .../Image/Resource/JsonPatch/Operation.php | 148 +++ .../lib/OpenCloud/Image/Resource/Member.php | 93 ++ .../Resource/Schema/AbstractSchemaItem.php | 38 + .../Image/Resource/Schema/Property.php | 232 +++++ .../Image/Resource/Schema/Schema.php | 193 ++++ .../lib/OpenCloud/Image/Service.php | 137 +++ .../Collection/LoadBalancerIterator.php | 39 + .../OpenCloud/LoadBalancer/Enum/IpType.php | 35 + .../LoadBalancer/Enum/NodeCondition.php | 30 + .../OpenCloud/LoadBalancer/Enum/NodeType.php | 29 + .../Resource/AbstractResource.php | 71 ++ .../LoadBalancer/Resource/Access.php | 77 +- .../LoadBalancer/Resource/Algorithm.php | 59 +- .../LoadBalancer/Resource/AllowedDomain.php | 32 +- .../Resource/BillableLoadBalancer.php | 22 - .../Resource/CertificateMapping.php | 96 ++ .../Resource/ConnectionLogging.php | 44 +- .../Resource/ConnectionThrottle.php | 72 +- .../LoadBalancer/Resource/ContentCaching.php | 57 +- .../LoadBalancer/Resource/ErrorPage.php | 56 +- .../LoadBalancer/Resource/HealthMonitor.php | 120 ++- .../LoadBalancer/Resource/LoadBalancer.php | 652 +++++++----- .../LoadBalancer/Resource/Metadata.php | 28 +- .../OpenCloud/LoadBalancer/Resource/Node.php | 164 +-- .../LoadBalancer/Resource/NodeEvent.php | 26 +- .../Resource/NonIdUriResource.php | 32 + .../LoadBalancer/Resource/Protocol.php | 33 +- .../Resource/ReadOnlyResource.php | 42 + .../LoadBalancer/Resource/Readonly.php | 37 - .../LoadBalancer/Resource/SSLTermination.php | 95 +- .../Resource/SessionPersistence.php | 54 +- .../OpenCloud/LoadBalancer/Resource/Stats.php | 71 +- .../LoadBalancer/Resource/SubResource.php | 109 -- .../OpenCloud/LoadBalancer/Resource/Usage.php | 50 - .../LoadBalancer/Resource/UsageRecord.php | 59 ++ .../LoadBalancer/Resource/VirtualIp.php | 62 +- .../lib/OpenCloud/LoadBalancer/Service.php | 151 +-- .../OpenCloud/Networking/Resource/Network.php | 77 ++ .../Networking/Resource/NetworkInterface.php | 31 + .../OpenCloud/Networking/Resource/Port.php | 94 ++ .../Networking/Resource/SecurityGroup.php | 69 ++ .../Networking/Resource/SecurityGroupRule.php | 86 ++ .../OpenCloud/Networking/Resource/Subnet.php | 89 ++ .../lib/OpenCloud/Networking/Service.php | 412 ++++++++ .../OpenCloud/ObjectStore/AbstractService.php | 41 +- .../lib/OpenCloud/ObjectStore/CDNService.php | 67 +- .../ObjectStore/Constants/Header.php | 26 +- .../ObjectStore/Constants/UrlType.php | 22 +- .../OpenCloud/ObjectStore/Enum/ReturnType.php | 29 + .../Exception/BulkOperationException.php | 24 +- .../Exception/ContainerException.php | 18 +- .../Exception/ObjectNotEmptyException.php | 35 + .../Exception/ObjectNotFoundException.php | 19 +- .../ObjectStore/Exception/StreamException.php | 22 +- .../ObjectStore/Exception/UploadException.php | 20 +- .../Resource/AbstractContainer.php | 77 +- .../ObjectStore/Resource/AbstractResource.php | 134 ++- .../ObjectStore/Resource/Account.php | 37 +- .../ObjectStore/Resource/CDNContainer.php | 42 +- .../ObjectStore/Resource/Container.php | 302 ++++-- .../Resource/ContainerMetadata.php | 21 +- .../ObjectStore/Resource/DataObject.php | 199 +++- .../lib/OpenCloud/ObjectStore/Service.php | 168 +++- .../ObjectStore/Upload/AbstractTransfer.php | 52 +- .../ObjectStore/Upload/ConcurrentTransfer.php | 44 +- .../Upload/ConsecutiveTransfer.php | 30 +- .../ObjectStore/Upload/ContainerMigration.php | 25 +- .../ObjectStore/Upload/DirectorySync.php | 62 +- .../ObjectStore/Upload/TransferBuilder.php | 49 +- .../ObjectStore/Upload/TransferPart.php | 53 +- .../ObjectStore/Upload/TransferState.php | 27 +- .../php-opencloud/lib/OpenCloud/OpenStack.php | 217 ++-- .../lib/OpenCloud/Orchestration/Resource.php | 79 -- .../Orchestration/Resource/BuildInfo.php | 40 + .../Orchestration/Resource/Event.php | 54 + .../Orchestration/Resource/Resource.php | 95 ++ .../Orchestration/Resource/ResourceType.php | 61 ++ .../Orchestration/Resource/Stack.php | 234 +++++ .../lib/OpenCloud/Orchestration/Service.php | 170 +++- .../lib/OpenCloud/Orchestration/Stack.php | 196 ---- .../Queues/Collection/MessageIterator.php | 29 + .../Exception/DeleteMessageException.php | 23 +- .../Queues/Exception/MessageException.php | 21 +- .../Queues/Exception/QueueException.php | 23 +- .../Exception/QueueMetadataException.php | 23 +- .../lib/OpenCloud/Queues/Resource/Claim.php | 88 +- .../lib/OpenCloud/Queues/Resource/Message.php | 98 +- .../lib/OpenCloud/Queues/Resource/Queue.php | 223 ++--- .../lib/OpenCloud/Queues/Service.php | 118 ++- .../php-opencloud/lib/OpenCloud/Rackspace.php | 68 +- .../php-opencloud/lib/OpenCloud/Version.php | 25 +- .../OpenCloud/Volume/Resource/Snapshot.php | 64 +- .../lib/OpenCloud/Volume/Resource/Volume.php | 90 +- .../OpenCloud/Volume/Resource/VolumeType.php | 60 +- .../lib/OpenCloud/Volume/Service.php | 157 +-- rackspace/php-opencloud/lib/php-opencloud.php | 17 +- rackspace/php-opencloud/phpunit.xml.dist | 5 + 901 files changed, 59522 insertions(+), 10540 deletions(-) create mode 100644 guzzle/guzzle/.gitignore create mode 100644 guzzle/guzzle/CHANGELOG.md create mode 100644 guzzle/guzzle/LICENSE create mode 100644 guzzle/guzzle/README.md create mode 100644 guzzle/guzzle/UPGRADING.md create mode 100644 guzzle/guzzle/build.xml create mode 100644 guzzle/guzzle/composer.json create mode 100644 guzzle/guzzle/docs/Makefile create mode 100644 guzzle/guzzle/docs/_downloads/guzzle-schema-1.0.json create mode 100644 guzzle/guzzle/docs/_static/guzzle-icon.png create mode 100644 guzzle/guzzle/docs/_static/homepage.css create mode 100644 guzzle/guzzle/docs/_static/logo.png create mode 100644 guzzle/guzzle/docs/_static/prettify.css create mode 100644 guzzle/guzzle/docs/_static/prettify.js create mode 100644 guzzle/guzzle/docs/_templates/index.html create mode 100644 guzzle/guzzle/docs/_templates/leftbar.html create mode 100644 guzzle/guzzle/docs/_templates/nav_links.html create mode 100644 guzzle/guzzle/docs/batching/batching.rst create mode 100644 guzzle/guzzle/docs/conf.py create mode 100644 guzzle/guzzle/docs/docs.rst create mode 100644 guzzle/guzzle/docs/getting-started/faq.rst create mode 100644 guzzle/guzzle/docs/getting-started/installation.rst create mode 100644 guzzle/guzzle/docs/getting-started/overview.rst create mode 100644 guzzle/guzzle/docs/http-client/client.rst create mode 100644 guzzle/guzzle/docs/http-client/entity-bodies.rst create mode 100644 guzzle/guzzle/docs/http-client/http-redirects.rst create mode 100644 guzzle/guzzle/docs/http-client/request.rst create mode 100644 guzzle/guzzle/docs/http-client/response.rst create mode 100644 guzzle/guzzle/docs/http-client/uri-templates.rst create mode 100644 guzzle/guzzle/docs/index.rst create mode 100644 guzzle/guzzle/docs/iterators/guzzle-iterators.rst create mode 100644 guzzle/guzzle/docs/iterators/resource-iterators.rst create mode 100644 guzzle/guzzle/docs/plugins/async-plugin.rst create mode 100644 guzzle/guzzle/docs/plugins/backoff-plugin.rst create mode 100644 guzzle/guzzle/docs/plugins/cache-plugin.rst create mode 100644 guzzle/guzzle/docs/plugins/cookie-plugin.rst create mode 100644 guzzle/guzzle/docs/plugins/creating-plugins.rst create mode 100644 guzzle/guzzle/docs/plugins/curl-auth-plugin.rst create mode 100644 guzzle/guzzle/docs/plugins/history-plugin.rst create mode 100644 guzzle/guzzle/docs/plugins/log-plugin.rst create mode 100644 guzzle/guzzle/docs/plugins/md5-validator-plugin.rst create mode 100644 guzzle/guzzle/docs/plugins/mock-plugin.rst create mode 100644 guzzle/guzzle/docs/plugins/oauth-plugin.rst create mode 100644 guzzle/guzzle/docs/plugins/plugins-list.rst.inc create mode 100644 guzzle/guzzle/docs/plugins/plugins-overview.rst create mode 100644 guzzle/guzzle/docs/requirements.txt create mode 100644 guzzle/guzzle/docs/testing/unit-testing.rst create mode 100644 guzzle/guzzle/docs/webservice-client/guzzle-service-descriptions.rst create mode 100644 guzzle/guzzle/docs/webservice-client/using-the-service-builder.rst create mode 100644 guzzle/guzzle/docs/webservice-client/webservice-client.rst create mode 100644 guzzle/guzzle/phar-stub.php create mode 100644 guzzle/guzzle/phing/build.properties.dist create mode 100644 guzzle/guzzle/phing/imports/dependencies.xml create mode 100644 guzzle/guzzle/phing/imports/deploy.xml create mode 100644 guzzle/guzzle/phing/tasks/ComposerLintTask.php create mode 100644 guzzle/guzzle/phing/tasks/GuzzlePearPharPackageTask.php create mode 100644 guzzle/guzzle/phing/tasks/GuzzleSubSplitTask.php create mode 100644 guzzle/guzzle/phpunit.xml.dist create mode 100644 guzzle/guzzle/src/Guzzle/Batch/AbstractBatchDecorator.php create mode 100644 guzzle/guzzle/src/Guzzle/Batch/Batch.php create mode 100644 guzzle/guzzle/src/Guzzle/Batch/BatchBuilder.php create mode 100644 guzzle/guzzle/src/Guzzle/Batch/BatchClosureDivisor.php create mode 100644 guzzle/guzzle/src/Guzzle/Batch/BatchClosureTransfer.php create mode 100644 guzzle/guzzle/src/Guzzle/Batch/BatchCommandTransfer.php create mode 100644 guzzle/guzzle/src/Guzzle/Batch/BatchDivisorInterface.php create mode 100644 guzzle/guzzle/src/Guzzle/Batch/BatchInterface.php create mode 100644 guzzle/guzzle/src/Guzzle/Batch/BatchRequestTransfer.php create mode 100644 guzzle/guzzle/src/Guzzle/Batch/BatchSizeDivisor.php create mode 100644 guzzle/guzzle/src/Guzzle/Batch/BatchTransferInterface.php create mode 100644 guzzle/guzzle/src/Guzzle/Batch/Exception/BatchTransferException.php create mode 100644 guzzle/guzzle/src/Guzzle/Batch/ExceptionBufferingBatch.php create mode 100644 guzzle/guzzle/src/Guzzle/Batch/FlushingBatch.php create mode 100644 guzzle/guzzle/src/Guzzle/Batch/HistoryBatch.php create mode 100644 guzzle/guzzle/src/Guzzle/Batch/NotifyingBatch.php create mode 100644 guzzle/guzzle/src/Guzzle/Batch/composer.json create mode 100644 guzzle/guzzle/src/Guzzle/Cache/AbstractCacheAdapter.php create mode 100644 guzzle/guzzle/src/Guzzle/Cache/CacheAdapterFactory.php create mode 100644 guzzle/guzzle/src/Guzzle/Cache/CacheAdapterInterface.php create mode 100644 guzzle/guzzle/src/Guzzle/Cache/ClosureCacheAdapter.php create mode 100644 guzzle/guzzle/src/Guzzle/Cache/DoctrineCacheAdapter.php create mode 100644 guzzle/guzzle/src/Guzzle/Cache/NullCacheAdapter.php create mode 100644 guzzle/guzzle/src/Guzzle/Cache/Zf1CacheAdapter.php create mode 100644 guzzle/guzzle/src/Guzzle/Cache/Zf2CacheAdapter.php create mode 100644 guzzle/guzzle/src/Guzzle/Cache/composer.json rename guzzle/{common => guzzle/src}/Guzzle/Common/AbstractHasDispatcher.php (100%) rename guzzle/{common => guzzle/src}/Guzzle/Common/Collection.php (100%) rename guzzle/{common => guzzle/src}/Guzzle/Common/Event.php (100%) rename guzzle/{common => guzzle/src}/Guzzle/Common/Exception/BadMethodCallException.php (100%) rename guzzle/{common => guzzle/src}/Guzzle/Common/Exception/ExceptionCollection.php (100%) rename guzzle/{common => guzzle/src}/Guzzle/Common/Exception/GuzzleException.php (100%) rename guzzle/{common => guzzle/src}/Guzzle/Common/Exception/InvalidArgumentException.php (100%) rename guzzle/{common => guzzle/src}/Guzzle/Common/Exception/RuntimeException.php (100%) rename guzzle/{common => guzzle/src}/Guzzle/Common/Exception/UnexpectedValueException.php (100%) rename guzzle/{common => guzzle/src}/Guzzle/Common/FromConfigInterface.php (100%) rename guzzle/{common => guzzle/src}/Guzzle/Common/HasDispatcherInterface.php (100%) rename guzzle/{common => guzzle/src}/Guzzle/Common/ToArrayInterface.php (100%) rename guzzle/{common => guzzle/src}/Guzzle/Common/Version.php (100%) rename guzzle/{common => guzzle/src}/Guzzle/Common/composer.json (100%) rename guzzle/{http => guzzle/src}/Guzzle/Http/AbstractEntityBodyDecorator.php (100%) rename guzzle/{http => guzzle/src}/Guzzle/Http/CachingEntityBody.php (100%) rename guzzle/{http => guzzle/src}/Guzzle/Http/Client.php (100%) rename guzzle/{http => guzzle/src}/Guzzle/Http/ClientInterface.php (100%) rename guzzle/{http => guzzle/src}/Guzzle/Http/Curl/CurlHandle.php (100%) rename guzzle/{http => guzzle/src}/Guzzle/Http/Curl/CurlMulti.php (100%) rename guzzle/{http => guzzle/src}/Guzzle/Http/Curl/CurlMultiInterface.php (100%) rename guzzle/{http => guzzle/src}/Guzzle/Http/Curl/CurlMultiProxy.php (100%) rename guzzle/{http => guzzle/src}/Guzzle/Http/Curl/CurlVersion.php (100%) rename guzzle/{http => guzzle/src}/Guzzle/Http/Curl/RequestMediator.php (100%) rename guzzle/{http => guzzle/src}/Guzzle/Http/EntityBody.php (100%) rename guzzle/{http => guzzle/src}/Guzzle/Http/EntityBodyInterface.php (100%) rename guzzle/{http => guzzle/src}/Guzzle/Http/Exception/BadResponseException.php (100%) rename guzzle/{http => guzzle/src}/Guzzle/Http/Exception/ClientErrorResponseException.php (100%) rename guzzle/{http => guzzle/src}/Guzzle/Http/Exception/CouldNotRewindStreamException.php (100%) rename guzzle/{http => guzzle/src}/Guzzle/Http/Exception/CurlException.php (100%) rename guzzle/{http => guzzle/src}/Guzzle/Http/Exception/HttpException.php (100%) rename guzzle/{http => guzzle/src}/Guzzle/Http/Exception/MultiTransferException.php (100%) rename guzzle/{http => guzzle/src}/Guzzle/Http/Exception/RequestException.php (100%) rename guzzle/{http => guzzle/src}/Guzzle/Http/Exception/ServerErrorResponseException.php (100%) rename guzzle/{http => guzzle/src}/Guzzle/Http/Exception/TooManyRedirectsException.php (100%) rename guzzle/{http => guzzle/src}/Guzzle/Http/IoEmittingEntityBody.php (100%) rename guzzle/{http => guzzle/src}/Guzzle/Http/Message/AbstractMessage.php (100%) rename guzzle/{http => guzzle/src}/Guzzle/Http/Message/EntityEnclosingRequest.php (100%) rename guzzle/{http => guzzle/src}/Guzzle/Http/Message/EntityEnclosingRequestInterface.php (100%) rename guzzle/{http => guzzle/src}/Guzzle/Http/Message/Header.php (100%) rename guzzle/{http => guzzle/src}/Guzzle/Http/Message/Header/CacheControl.php (100%) rename guzzle/{http => guzzle/src}/Guzzle/Http/Message/Header/HeaderCollection.php (100%) rename guzzle/{http => guzzle/src}/Guzzle/Http/Message/Header/HeaderFactory.php (100%) rename guzzle/{http => guzzle/src}/Guzzle/Http/Message/Header/HeaderFactoryInterface.php (100%) rename guzzle/{http => guzzle/src}/Guzzle/Http/Message/Header/HeaderInterface.php (100%) rename guzzle/{http => guzzle/src}/Guzzle/Http/Message/Header/Link.php (100%) rename guzzle/{http => guzzle/src}/Guzzle/Http/Message/MessageInterface.php (100%) rename guzzle/{http => guzzle/src}/Guzzle/Http/Message/PostFile.php (100%) rename guzzle/{http => guzzle/src}/Guzzle/Http/Message/PostFileInterface.php (100%) rename guzzle/{http => guzzle/src}/Guzzle/Http/Message/Request.php (100%) rename guzzle/{http => guzzle/src}/Guzzle/Http/Message/RequestFactory.php (100%) rename guzzle/{http => guzzle/src}/Guzzle/Http/Message/RequestFactoryInterface.php (100%) rename guzzle/{http => guzzle/src}/Guzzle/Http/Message/RequestInterface.php (100%) rename guzzle/{http => guzzle/src}/Guzzle/Http/Message/Response.php (100%) rename guzzle/{http => guzzle/src}/Guzzle/Http/Mimetypes.php (100%) rename guzzle/{http => guzzle/src}/Guzzle/Http/QueryAggregator/CommaAggregator.php (100%) rename guzzle/{http => guzzle/src}/Guzzle/Http/QueryAggregator/DuplicateAggregator.php (100%) rename guzzle/{http => guzzle/src}/Guzzle/Http/QueryAggregator/PhpAggregator.php (100%) rename guzzle/{http => guzzle/src}/Guzzle/Http/QueryAggregator/QueryAggregatorInterface.php (100%) rename guzzle/{http => guzzle/src}/Guzzle/Http/QueryString.php (100%) rename guzzle/{http => guzzle/src}/Guzzle/Http/ReadLimitEntityBody.php (100%) rename guzzle/{http => guzzle/src}/Guzzle/Http/RedirectPlugin.php (100%) rename guzzle/{http => guzzle/src}/Guzzle/Http/Resources/cacert.pem (100%) rename guzzle/{http => guzzle/src}/Guzzle/Http/Resources/cacert.pem.md5 (100%) rename guzzle/{http => guzzle/src}/Guzzle/Http/StaticClient.php (100%) rename guzzle/{http => guzzle/src}/Guzzle/Http/Url.php (100%) rename guzzle/{http => guzzle/src}/Guzzle/Http/composer.json (100%) create mode 100644 guzzle/guzzle/src/Guzzle/Inflection/Inflector.php create mode 100644 guzzle/guzzle/src/Guzzle/Inflection/InflectorInterface.php create mode 100644 guzzle/guzzle/src/Guzzle/Inflection/MemoizingInflector.php create mode 100644 guzzle/guzzle/src/Guzzle/Inflection/PreComputedInflector.php create mode 100644 guzzle/guzzle/src/Guzzle/Inflection/composer.json create mode 100644 guzzle/guzzle/src/Guzzle/Iterator/AppendIterator.php create mode 100644 guzzle/guzzle/src/Guzzle/Iterator/ChunkedIterator.php create mode 100644 guzzle/guzzle/src/Guzzle/Iterator/FilterIterator.php create mode 100644 guzzle/guzzle/src/Guzzle/Iterator/MapIterator.php create mode 100644 guzzle/guzzle/src/Guzzle/Iterator/MethodProxyIterator.php create mode 100644 guzzle/guzzle/src/Guzzle/Iterator/README.md create mode 100644 guzzle/guzzle/src/Guzzle/Iterator/composer.json create mode 100644 guzzle/guzzle/src/Guzzle/Log/AbstractLogAdapter.php create mode 100644 guzzle/guzzle/src/Guzzle/Log/ArrayLogAdapter.php create mode 100644 guzzle/guzzle/src/Guzzle/Log/ClosureLogAdapter.php create mode 100644 guzzle/guzzle/src/Guzzle/Log/LogAdapterInterface.php create mode 100644 guzzle/guzzle/src/Guzzle/Log/MessageFormatter.php create mode 100644 guzzle/guzzle/src/Guzzle/Log/MonologLogAdapter.php create mode 100644 guzzle/guzzle/src/Guzzle/Log/PsrLogAdapter.php create mode 100644 guzzle/guzzle/src/Guzzle/Log/Zf1LogAdapter.php create mode 100644 guzzle/guzzle/src/Guzzle/Log/Zf2LogAdapter.php create mode 100644 guzzle/guzzle/src/Guzzle/Log/composer.json rename guzzle/{parser => guzzle/src}/Guzzle/Parser/Cookie/CookieParser.php (100%) rename guzzle/{parser => guzzle/src}/Guzzle/Parser/Cookie/CookieParserInterface.php (100%) rename guzzle/{parser => guzzle/src}/Guzzle/Parser/Message/AbstractMessageParser.php (100%) rename guzzle/{parser => guzzle/src}/Guzzle/Parser/Message/MessageParser.php (100%) rename guzzle/{parser => guzzle/src}/Guzzle/Parser/Message/MessageParserInterface.php (100%) rename guzzle/{parser => guzzle/src}/Guzzle/Parser/Message/PeclHttpMessageParser.php (100%) rename guzzle/{parser => guzzle/src}/Guzzle/Parser/ParserRegistry.php (100%) rename guzzle/{parser => guzzle/src}/Guzzle/Parser/UriTemplate/PeclUriTemplate.php (100%) rename guzzle/{parser => guzzle/src}/Guzzle/Parser/UriTemplate/UriTemplate.php (100%) rename guzzle/{parser => guzzle/src}/Guzzle/Parser/UriTemplate/UriTemplateInterface.php (100%) rename guzzle/{parser => guzzle/src}/Guzzle/Parser/Url/UrlParser.php (100%) rename guzzle/{parser => guzzle/src}/Guzzle/Parser/Url/UrlParserInterface.php (100%) rename guzzle/{parser => guzzle/src}/Guzzle/Parser/composer.json (100%) create mode 100644 guzzle/guzzle/src/Guzzle/Plugin/Async/AsyncPlugin.php create mode 100644 guzzle/guzzle/src/Guzzle/Plugin/Async/composer.json create mode 100644 guzzle/guzzle/src/Guzzle/Plugin/Backoff/AbstractBackoffStrategy.php create mode 100644 guzzle/guzzle/src/Guzzle/Plugin/Backoff/AbstractErrorCodeBackoffStrategy.php create mode 100644 guzzle/guzzle/src/Guzzle/Plugin/Backoff/BackoffLogger.php create mode 100644 guzzle/guzzle/src/Guzzle/Plugin/Backoff/BackoffPlugin.php create mode 100644 guzzle/guzzle/src/Guzzle/Plugin/Backoff/BackoffStrategyInterface.php create mode 100644 guzzle/guzzle/src/Guzzle/Plugin/Backoff/CallbackBackoffStrategy.php create mode 100644 guzzle/guzzle/src/Guzzle/Plugin/Backoff/ConstantBackoffStrategy.php create mode 100644 guzzle/guzzle/src/Guzzle/Plugin/Backoff/CurlBackoffStrategy.php create mode 100644 guzzle/guzzle/src/Guzzle/Plugin/Backoff/ExponentialBackoffStrategy.php create mode 100644 guzzle/guzzle/src/Guzzle/Plugin/Backoff/HttpBackoffStrategy.php create mode 100644 guzzle/guzzle/src/Guzzle/Plugin/Backoff/LinearBackoffStrategy.php create mode 100644 guzzle/guzzle/src/Guzzle/Plugin/Backoff/ReasonPhraseBackoffStrategy.php create mode 100644 guzzle/guzzle/src/Guzzle/Plugin/Backoff/TruncatedBackoffStrategy.php create mode 100644 guzzle/guzzle/src/Guzzle/Plugin/Backoff/composer.json create mode 100644 guzzle/guzzle/src/Guzzle/Plugin/Cache/CacheKeyProviderInterface.php create mode 100644 guzzle/guzzle/src/Guzzle/Plugin/Cache/CachePlugin.php create mode 100644 guzzle/guzzle/src/Guzzle/Plugin/Cache/CacheStorageInterface.php create mode 100644 guzzle/guzzle/src/Guzzle/Plugin/Cache/CallbackCanCacheStrategy.php create mode 100644 guzzle/guzzle/src/Guzzle/Plugin/Cache/CanCacheStrategyInterface.php create mode 100644 guzzle/guzzle/src/Guzzle/Plugin/Cache/DefaultCacheKeyProvider.php create mode 100644 guzzle/guzzle/src/Guzzle/Plugin/Cache/DefaultCacheStorage.php create mode 100644 guzzle/guzzle/src/Guzzle/Plugin/Cache/DefaultCanCacheStrategy.php create mode 100644 guzzle/guzzle/src/Guzzle/Plugin/Cache/DefaultRevalidation.php create mode 100644 guzzle/guzzle/src/Guzzle/Plugin/Cache/DenyRevalidation.php create mode 100644 guzzle/guzzle/src/Guzzle/Plugin/Cache/RevalidationInterface.php create mode 100644 guzzle/guzzle/src/Guzzle/Plugin/Cache/SkipRevalidation.php create mode 100644 guzzle/guzzle/src/Guzzle/Plugin/Cache/composer.json create mode 100644 guzzle/guzzle/src/Guzzle/Plugin/Cookie/Cookie.php create mode 100644 guzzle/guzzle/src/Guzzle/Plugin/Cookie/CookieJar/ArrayCookieJar.php create mode 100644 guzzle/guzzle/src/Guzzle/Plugin/Cookie/CookieJar/CookieJarInterface.php create mode 100644 guzzle/guzzle/src/Guzzle/Plugin/Cookie/CookieJar/FileCookieJar.php create mode 100644 guzzle/guzzle/src/Guzzle/Plugin/Cookie/CookiePlugin.php create mode 100644 guzzle/guzzle/src/Guzzle/Plugin/Cookie/Exception/InvalidCookieException.php create mode 100644 guzzle/guzzle/src/Guzzle/Plugin/Cookie/composer.json create mode 100644 guzzle/guzzle/src/Guzzle/Plugin/CurlAuth/CurlAuthPlugin.php create mode 100644 guzzle/guzzle/src/Guzzle/Plugin/CurlAuth/composer.json create mode 100644 guzzle/guzzle/src/Guzzle/Plugin/ErrorResponse/ErrorResponseExceptionInterface.php create mode 100644 guzzle/guzzle/src/Guzzle/Plugin/ErrorResponse/ErrorResponsePlugin.php create mode 100644 guzzle/guzzle/src/Guzzle/Plugin/ErrorResponse/Exception/ErrorResponseException.php create mode 100644 guzzle/guzzle/src/Guzzle/Plugin/ErrorResponse/composer.json create mode 100644 guzzle/guzzle/src/Guzzle/Plugin/History/HistoryPlugin.php create mode 100644 guzzle/guzzle/src/Guzzle/Plugin/History/composer.json create mode 100644 guzzle/guzzle/src/Guzzle/Plugin/Log/LogPlugin.php create mode 100644 guzzle/guzzle/src/Guzzle/Plugin/Log/composer.json create mode 100644 guzzle/guzzle/src/Guzzle/Plugin/Md5/CommandContentMd5Plugin.php create mode 100644 guzzle/guzzle/src/Guzzle/Plugin/Md5/Md5ValidatorPlugin.php create mode 100644 guzzle/guzzle/src/Guzzle/Plugin/Md5/composer.json create mode 100644 guzzle/guzzle/src/Guzzle/Plugin/Mock/MockPlugin.php create mode 100644 guzzle/guzzle/src/Guzzle/Plugin/Mock/composer.json create mode 100644 guzzle/guzzle/src/Guzzle/Plugin/Oauth/OauthPlugin.php create mode 100644 guzzle/guzzle/src/Guzzle/Plugin/Oauth/composer.json create mode 100644 guzzle/guzzle/src/Guzzle/Plugin/composer.json create mode 100644 guzzle/guzzle/src/Guzzle/Service/AbstractConfigLoader.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Builder/ServiceBuilder.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Builder/ServiceBuilderInterface.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Builder/ServiceBuilderLoader.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/CachingConfigLoader.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Client.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/ClientInterface.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Command/AbstractCommand.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Command/ClosureCommand.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Command/CommandInterface.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Command/CreateResponseClassEvent.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Command/DefaultRequestSerializer.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Command/DefaultResponseParser.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Command/Factory/AliasFactory.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Command/Factory/CompositeFactory.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Command/Factory/ConcreteClassFactory.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Command/Factory/FactoryInterface.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Command/Factory/MapFactory.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Command/Factory/ServiceDescriptionFactory.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/AbstractRequestVisitor.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/BodyVisitor.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/HeaderVisitor.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/JsonVisitor.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/PostFieldVisitor.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/PostFileVisitor.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/QueryVisitor.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/RequestVisitorInterface.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/ResponseBodyVisitor.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/XmlVisitor.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Response/AbstractResponseVisitor.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Response/BodyVisitor.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Response/HeaderVisitor.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Response/JsonVisitor.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Response/ReasonPhraseVisitor.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Response/ResponseVisitorInterface.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Response/StatusCodeVisitor.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Response/XmlVisitor.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/VisitorFlyweight.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Command/OperationCommand.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Command/OperationResponseParser.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Command/RequestSerializerInterface.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Command/ResponseClassInterface.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Command/ResponseParserInterface.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/ConfigLoaderInterface.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Description/Operation.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Description/OperationInterface.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Description/Parameter.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Description/SchemaFormatter.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Description/SchemaValidator.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Description/ServiceDescription.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Description/ServiceDescriptionInterface.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Description/ServiceDescriptionLoader.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Description/ValidatorInterface.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Exception/CommandException.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Exception/CommandTransferException.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Exception/DescriptionBuilderException.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Exception/InconsistentClientTransferException.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Exception/ResponseClassException.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Exception/ServiceBuilderException.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Exception/ServiceNotFoundException.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Exception/ValidationException.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Resource/AbstractResourceIteratorFactory.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Resource/CompositeResourceIteratorFactory.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Resource/MapResourceIteratorFactory.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Resource/Model.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Resource/ResourceIterator.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Resource/ResourceIteratorApplyBatched.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Resource/ResourceIteratorClassFactory.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Resource/ResourceIteratorFactoryInterface.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/Resource/ResourceIteratorInterface.php create mode 100644 guzzle/guzzle/src/Guzzle/Service/composer.json rename guzzle/{stream => guzzle/src}/Guzzle/Stream/PhpStreamRequestFactory.php (100%) rename guzzle/{stream => guzzle/src}/Guzzle/Stream/Stream.php (100%) rename guzzle/{stream => guzzle/src}/Guzzle/Stream/StreamInterface.php (100%) rename guzzle/{stream => guzzle/src}/Guzzle/Stream/StreamRequestFactoryInterface.php (100%) rename guzzle/{stream => guzzle/src}/Guzzle/Stream/composer.json (100%) create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Batch/AbstractBatchDecoratorTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Batch/BatchBuilderTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Batch/BatchClosureDivisorTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Batch/BatchClosureTransferTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Batch/BatchCommandTransferTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Batch/BatchRequestTransferTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Batch/BatchSizeDivisorTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Batch/BatchTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Batch/ExceptionBufferingBatchTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Batch/FlushingBatchTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Batch/HistoryBatchTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Batch/NotifyingBatchTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Cache/CacheAdapterFactoryTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Cache/CacheAdapterTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Cache/ClosureCacheAdapterTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Cache/NullCacheAdapterTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Cache/Zf2CacheAdapterTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Common/AbstractHasDispatcherTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Common/CollectionTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Common/EventTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Common/Exception/BatchTransferExceptionTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Common/Exception/ExceptionCollectionTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Common/VersionTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/GuzzleTestCase.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Http/AbstractEntityBodyDecoratorTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Http/CachingEntityBodyTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Http/ClientTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Http/Curl/CurlHandleTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Http/Curl/CurlMultiProxyTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Http/Curl/CurlMultiTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Http/Curl/CurlVersionTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Http/Curl/RequestMediatorTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Http/EntityBodyTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Http/Exception/CurlExceptionTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Http/Exception/ExceptionTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Http/Exception/MultiTransferExceptionTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Http/IoEmittingEntityBodyTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Http/Message/AbstractMessageTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Http/Message/EntityEnclosingRequestTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Http/Message/Header/HeaderFactoryTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Http/Message/Header/LinkTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Http/Message/HeaderComparison.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Http/Message/HeaderComparisonTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Http/Message/HeaderTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Http/Message/PostFileTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Http/Message/RequestFactoryTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Http/Message/RequestTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Http/Message/ResponseTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Http/MimetypesTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Http/QueryAggregator/CommaAggregatorTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Http/QueryAggregator/DuplicateAggregatorTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Http/QueryAggregator/PhpAggregatorTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Http/QueryStringTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Http/ReadLimitEntityBodyTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Http/RedirectPluginTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Http/Server.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Http/StaticClientTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Http/UrlTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Http/server.js create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Inflection/InflectorTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Inflection/MemoizingInflectorTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Inflection/PreComputedInflectorTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Iterator/AppendIteratorTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Iterator/ChunkedIteratorTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Iterator/FilterIteratorTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Iterator/MapIteratorTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Iterator/MethodProxyIteratorTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Log/ArrayLogAdapterTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Log/ClosureLogAdapterTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Log/MessageFormatterTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Log/PsrLogAdapterTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Log/Zf2LogAdapterTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Mock/CustomResponseModel.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Mock/ErrorResponseMock.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Mock/ExceptionMock.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Mock/MockMulti.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Mock/MockObserver.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Mock/MockSubject.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Parser/Cookie/CookieParserProvider.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Parser/Cookie/CookieParserTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Parser/Message/MessageParserProvider.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Parser/Message/MessageParserTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Parser/Message/PeclHttpMessageParserTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Parser/ParserRegistryTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Parser/UriTemplate/AbstractUriTemplateTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Parser/UriTemplate/PeclUriTemplateTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Parser/UriTemplate/UriTemplateTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Plugin/Async/AsyncPluginTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/AbstractBackoffStrategyTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/BackoffLoggerTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/BackoffPluginTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/CallbackBackoffStrategyTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/ConstantBackoffStrategyTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/CurlBackoffStrategyTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/ExponentialBackoffStrategyTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/HttpBackoffStrategyTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/LinearBackoffStrategyTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/ReasonPhraseBackoffStrategyTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/TruncatedBackoffStrategyTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cache/CachePluginTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cache/CallbackCanCacheStrategyTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cache/DefaultCacheStorageTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cache/DefaultCanCacheStrategyTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cache/DefaultRevalidationTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cache/DenyRevalidationTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cache/SkipRevalidationTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cookie/CookieJar/ArrayCookieJarTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cookie/CookieJar/FileCookieJarTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cookie/CookiePluginTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cookie/CookieTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Plugin/CurlAuth/CurlAuthPluginTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Plugin/ErrorResponse/ErrorResponsePluginTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Plugin/History/HistoryPluginTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Plugin/Log/LogPluginTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Plugin/Md5/CommandContentMd5PluginTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Plugin/Md5/Md5ValidatorPluginTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Plugin/Mock/MockPluginTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Plugin/Oauth/OauthPluginTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Service/AbstractConfigLoaderTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Service/Builder/ServiceBuilderLoaderTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Service/Builder/ServiceBuilderTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Service/CachingConfigLoaderTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Service/ClientTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Service/Command/AbstractCommandTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Service/Command/ClosureCommandTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Service/Command/CommandTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Service/Command/DefaultRequestSerializerTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Service/Command/DefaultResponseParserTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Service/Command/Factory/AliasFactoryTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Service/Command/Factory/CompositeFactoryTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Service/Command/Factory/ConcreteClassFactoryTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Service/Command/Factory/MapFactoryTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Service/Command/Factory/ServiceDescriptionFactoryTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Request/AbstractVisitorTestCase.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Request/BodyVisitorTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Request/HeaderVisitorTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Request/JsonVisitorTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Request/PostFieldVisitorTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Request/PostFileVisitorTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Request/QueryVisitorTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Request/ResponseBodyVisitorTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Request/XmlVisitorTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Response/AbstractResponseVisitorTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Response/BodyVisitorTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Response/HeaderVisitorTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Response/JsonVisitorTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Response/ReasonPhraseVisitorTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Response/StatusCodeVisitorTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Response/XmlVisitorTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/VisitorFlyweightTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Service/Command/OperationCommandTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Service/Command/OperationResponseParserTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Service/Description/OperationTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Service/Description/ParameterTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Service/Description/SchemaFormatterTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Service/Description/SchemaValidatorTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Service/Description/ServiceDescriptionLoaderTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Service/Description/ServiceDescriptionTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Service/Exception/CommandTransferExceptionTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Service/Exception/InconsistentClientTransferExceptionTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Service/Exception/ValidationExceptionTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Service/Mock/Command/IterableCommand.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Service/Mock/Command/MockCommand.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Service/Mock/Command/OtherCommand.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Service/Mock/Command/Sub/Sub.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Service/Mock/MockClient.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Service/Mock/Model/MockCommandIterator.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Service/Resource/CompositeResourceIteratorFactoryTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Service/Resource/MapResourceIteratorFactoryTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Service/Resource/ModelTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Service/Resource/ResourceIteratorClassFactoryTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Service/Resource/ResourceIteratorTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Stream/PhpStreamRequestFactoryTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/Stream/StreamTest.php create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/TestData/FileBody.txt create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/TestData/description/bar.json create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/TestData/description/baz.json create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/TestData/description/foo.json create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/TestData/description/recursive.json create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/TestData/mock_response create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/TestData/services/json1.json create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/TestData/services/json2.json create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/TestData/services/services.json create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/TestData/test_service.json create mode 100644 guzzle/guzzle/tests/Guzzle/Tests/TestData/test_service2.json create mode 100644 guzzle/guzzle/tests/bootstrap.php create mode 100644 mikemccabe/json-patch-php/.gitignore create mode 100644 mikemccabe/json-patch-php/.gitmodules create mode 100644 mikemccabe/json-patch-php/README.md create mode 100644 mikemccabe/json-patch-php/composer.json create mode 100644 mikemccabe/json-patch-php/local_tests.json create mode 100644 mikemccabe/json-patch-php/run_tests.php create mode 100644 mikemccabe/json-patch-php/simplexml_tests.json create mode 100644 mikemccabe/json-patch-php/src/JsonPatch.php create mode 100644 rackspace/php-opencloud/CODE_OF_CONDUCT.md create mode 100644 rackspace/php-opencloud/apigen.neon mode change 100755 => 100644 rackspace/php-opencloud/docs/userguide/Autoscale/Config.md mode change 100755 => 100644 rackspace/php-opencloud/docs/userguide/Autoscale/Groups.md mode change 100755 => 100644 rackspace/php-opencloud/docs/userguide/Autoscale/Policies.md mode change 100755 => 100644 rackspace/php-opencloud/docs/userguide/Autoscale/Webhooks.md mode change 100755 => 100644 rackspace/php-opencloud/docs/userguide/CloudMonitoring/Agents.md mode change 100755 => 100644 rackspace/php-opencloud/docs/userguide/CloudMonitoring/Alarms.md mode change 100755 => 100644 rackspace/php-opencloud/docs/userguide/CloudMonitoring/Changelogs.md mode change 100755 => 100644 rackspace/php-opencloud/docs/userguide/CloudMonitoring/Checks.md mode change 100755 => 100644 rackspace/php-opencloud/docs/userguide/CloudMonitoring/Entities.md mode change 100755 => 100644 rackspace/php-opencloud/docs/userguide/CloudMonitoring/Metrics.md mode change 100755 => 100644 rackspace/php-opencloud/docs/userguide/CloudMonitoring/Notifications.md mode change 100755 => 100644 rackspace/php-opencloud/docs/userguide/CloudMonitoring/Service.md mode change 100755 => 100644 rackspace/php-opencloud/docs/userguide/CloudMonitoring/Views.md mode change 100755 => 100644 rackspace/php-opencloud/docs/userguide/CloudMonitoring/Zones.md mode change 100755 => 100644 rackspace/php-opencloud/docs/userguide/ObjectStore/Access.md mode change 100755 => 100644 rackspace/php-opencloud/docs/userguide/ObjectStore/Account.md mode change 100755 => 100644 rackspace/php-opencloud/docs/userguide/ObjectStore/CDN/Container.md mode change 100755 => 100644 rackspace/php-opencloud/docs/userguide/ObjectStore/CDN/Object.md mode change 100755 => 100644 rackspace/php-opencloud/docs/userguide/ObjectStore/Storage/Container.md mode change 100755 => 100644 rackspace/php-opencloud/docs/userguide/ObjectStore/Storage/Object.md mode change 100755 => 100644 rackspace/php-opencloud/docs/userguide/Queues/Claim.md mode change 100755 => 100644 rackspace/php-opencloud/docs/userguide/Queues/Message.md mode change 100755 => 100644 rackspace/php-opencloud/docs/userguide/Queues/Queue.md mode change 100755 => 100644 rackspace/php-opencloud/docs/userguide/accessip.md mode change 100755 => 100644 rackspace/php-opencloud/docs/userguide/dbaas.md mode change 100755 => 100644 rackspace/php-opencloud/docs/userguide/flavors.md mode change 100755 => 100644 rackspace/php-opencloud/docs/userguide/networks.md mode change 100755 => 100644 rackspace/php-opencloud/docs/userguide/servers.md mode change 100755 => 100644 rackspace/php-opencloud/docs/userguide/volumes.md create mode 100644 rackspace/php-opencloud/lib/OpenCloud/CDN/Resource/Flavor.php create mode 100644 rackspace/php-opencloud/lib/OpenCloud/CDN/Resource/Service.php create mode 100644 rackspace/php-opencloud/lib/OpenCloud/CDN/Service.php create mode 100644 rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Collection/MonitoringIterator.php create mode 100644 rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/MetricDataPoint.php create mode 100644 rackspace/php-opencloud/lib/OpenCloud/Common/ArrayAccess.php create mode 100644 rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/BackupInstanceError.php create mode 100644 rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/BackupNameError.php create mode 100644 rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/DomainNotFoundException.php create mode 100644 rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/ForbiddenOperationException.php create mode 100644 rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/HttpResponseException.php create mode 100644 rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/InvalidTemplateError.php create mode 100644 rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/ResourceNotFoundException.php delete mode 100644 rackspace/php-opencloud/lib/OpenCloud/Common/Log/AbstractLogger.php delete mode 100644 rackspace/php-opencloud/lib/OpenCloud/Common/Log/LogLevel.php delete mode 100644 rackspace/php-opencloud/lib/OpenCloud/Common/Log/LoggerInterface.php create mode 100644 rackspace/php-opencloud/lib/OpenCloud/Common/Resource/BaseResource.php create mode 100644 rackspace/php-opencloud/lib/OpenCloud/Common/Resource/NovaResource.php create mode 100644 rackspace/php-opencloud/lib/OpenCloud/Common/Resource/PersistentResource.php create mode 100644 rackspace/php-opencloud/lib/OpenCloud/Common/Resource/ReadOnlyResource.php rename rackspace/php-opencloud/lib/OpenCloud/DNS/Resource/{Object.php => AbstractResource.php} (57%) create mode 100644 rackspace/php-opencloud/lib/OpenCloud/DNS/Resource/HasPtrRecordsInterface.php create mode 100644 rackspace/php-opencloud/lib/OpenCloud/Database/Resource/Backup.php create mode 100644 rackspace/php-opencloud/lib/OpenCloud/Database/Resource/Configuration.php create mode 100644 rackspace/php-opencloud/lib/OpenCloud/Database/Resource/Datastore.php create mode 100644 rackspace/php-opencloud/lib/OpenCloud/Database/Resource/DatastoreVersion.php create mode 100644 rackspace/php-opencloud/lib/OpenCloud/Image/Enum/Document.php create mode 100644 rackspace/php-opencloud/lib/OpenCloud/Image/Enum/MemberStatus.php create mode 100644 rackspace/php-opencloud/lib/OpenCloud/Image/Enum/OperationType.php create mode 100644 rackspace/php-opencloud/lib/OpenCloud/Image/Enum/Schema.php create mode 100644 rackspace/php-opencloud/lib/OpenCloud/Image/Resource/AbstractSchemaResource.php create mode 100644 rackspace/php-opencloud/lib/OpenCloud/Image/Resource/Image.php create mode 100644 rackspace/php-opencloud/lib/OpenCloud/Image/Resource/ImageInterface.php create mode 100644 rackspace/php-opencloud/lib/OpenCloud/Image/Resource/JsonPatch/Document.php create mode 100644 rackspace/php-opencloud/lib/OpenCloud/Image/Resource/JsonPatch/Encoder.php create mode 100644 rackspace/php-opencloud/lib/OpenCloud/Image/Resource/JsonPatch/Operation.php create mode 100644 rackspace/php-opencloud/lib/OpenCloud/Image/Resource/Member.php create mode 100644 rackspace/php-opencloud/lib/OpenCloud/Image/Resource/Schema/AbstractSchemaItem.php create mode 100644 rackspace/php-opencloud/lib/OpenCloud/Image/Resource/Schema/Property.php create mode 100644 rackspace/php-opencloud/lib/OpenCloud/Image/Resource/Schema/Schema.php create mode 100644 rackspace/php-opencloud/lib/OpenCloud/Image/Service.php create mode 100644 rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Collection/LoadBalancerIterator.php create mode 100644 rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Enum/IpType.php create mode 100644 rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Enum/NodeCondition.php create mode 100644 rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Enum/NodeType.php create mode 100644 rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/AbstractResource.php delete mode 100644 rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/BillableLoadBalancer.php create mode 100644 rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/CertificateMapping.php create mode 100644 rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/NonIdUriResource.php create mode 100644 rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/ReadOnlyResource.php delete mode 100644 rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/Readonly.php delete mode 100644 rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/SubResource.php delete mode 100644 rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/Usage.php create mode 100644 rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/UsageRecord.php create mode 100644 rackspace/php-opencloud/lib/OpenCloud/Networking/Resource/Network.php create mode 100644 rackspace/php-opencloud/lib/OpenCloud/Networking/Resource/NetworkInterface.php create mode 100644 rackspace/php-opencloud/lib/OpenCloud/Networking/Resource/Port.php create mode 100644 rackspace/php-opencloud/lib/OpenCloud/Networking/Resource/SecurityGroup.php create mode 100644 rackspace/php-opencloud/lib/OpenCloud/Networking/Resource/SecurityGroupRule.php create mode 100644 rackspace/php-opencloud/lib/OpenCloud/Networking/Resource/Subnet.php create mode 100644 rackspace/php-opencloud/lib/OpenCloud/Networking/Service.php create mode 100644 rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Enum/ReturnType.php create mode 100644 rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Exception/ObjectNotEmptyException.php delete mode 100644 rackspace/php-opencloud/lib/OpenCloud/Orchestration/Resource.php create mode 100644 rackspace/php-opencloud/lib/OpenCloud/Orchestration/Resource/BuildInfo.php create mode 100644 rackspace/php-opencloud/lib/OpenCloud/Orchestration/Resource/Event.php create mode 100644 rackspace/php-opencloud/lib/OpenCloud/Orchestration/Resource/Resource.php create mode 100644 rackspace/php-opencloud/lib/OpenCloud/Orchestration/Resource/ResourceType.php create mode 100644 rackspace/php-opencloud/lib/OpenCloud/Orchestration/Resource/Stack.php delete mode 100644 rackspace/php-opencloud/lib/OpenCloud/Orchestration/Stack.php create mode 100644 rackspace/php-opencloud/lib/OpenCloud/Queues/Collection/MessageIterator.php diff --git a/.gitignore b/.gitignore index fba4789f8..9fbeef7c5 100644 --- a/.gitignore +++ b/.gitignore @@ -197,6 +197,9 @@ ircmaxell/security-lib/tests bantu/ini-get-wrapper/tests rackspace/php-opencloud/tests +rackspace/php-opencloud/doc +rackspace/php-opencloud/docs +rackspace/php-opencloud/samples paragonie/random_compat/tests paragonie/random_compat/phpunit.sh diff --git a/composer.json b/composer.json index 280ccac22..10afb65d0 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,7 @@ "doctrine/dbal": "2.5.9", "mcnetic/zipstreamer": "^1.0", "phpseclib/phpseclib": "2.0.3", - "rackspace/php-opencloud": "v1.9.2", + "rackspace/php-opencloud": "v1.16.0", "jeremeamia/superclosure": "2.1.0", "bantu/ini-get-wrapper": "v1.0.1", "natxet/CssMin": "dev-master", diff --git a/composer.lock b/composer.lock index 056b1d062..58da19a97 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "dcb1337e082e0807a32ad1ec0539782c", + "content-hash": "c28ce28e99717ff65fb1d7f645e70a1f", "packages": [ { "name": "bantu/ini-get-wrapper", @@ -548,83 +548,67 @@ "time": "2014-09-09T13:34:57+00:00" }, { - "name": "guzzle/common", + "name": "guzzle/guzzle", "version": "v3.8.1", - "target-dir": "Guzzle/Common", "source": { "type": "git", - "url": "https://github.com/Guzzle3/common.git", - "reference": "67f6c3fd04bae387d47c2a673fa623ed8f4189bb" + "url": "https://github.com/guzzle/guzzle.git", + "reference": "4de0618a01b34aa1c8c33a3f13f396dcd3882eba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Guzzle3/common/zipball/67f6c3fd04bae387d47c2a673fa623ed8f4189bb", - "reference": "67f6c3fd04bae387d47c2a673fa623ed8f4189bb", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/4de0618a01b34aa1c8c33a3f13f396dcd3882eba", + "reference": "4de0618a01b34aa1c8c33a3f13f396dcd3882eba", "shasum": "" }, "require": { - "php": ">=5.3.2", + "ext-curl": "*", + "php": ">=5.3.3", "symfony/event-dispatcher": ">=2.1" }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.7-dev" - } - }, - "autoload": { - "psr-0": { - "Guzzle\\Common": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Common libraries used by Guzzle", - "homepage": "http://guzzlephp.org/", - "keywords": [ - "collection", - "common", - "event", - "exception" - ], - "abandoned": "guzzle/guzzle", - "time": "2014-01-28T22:29:15+00:00" - }, - { - "name": "guzzle/http", - "version": "v3.8.1", - "target-dir": "Guzzle/Http", - "source": { - "type": "git", - "url": "https://github.com/Guzzle3/http.git", - "reference": "565fd64be16d91c840f497c5de76f86d54a822d8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Guzzle3/http/zipball/565fd64be16d91c840f497c5de76f86d54a822d8", - "reference": "565fd64be16d91c840f497c5de76f86d54a822d8", - "shasum": "" - }, - "require": { + "replace": { + "guzzle/batch": "self.version", + "guzzle/cache": "self.version", "guzzle/common": "self.version", + "guzzle/http": "self.version", + "guzzle/inflection": "self.version", + "guzzle/iterator": "self.version", + "guzzle/log": "self.version", "guzzle/parser": "self.version", - "guzzle/stream": "self.version", - "php": ">=5.3.2" + "guzzle/plugin": "self.version", + "guzzle/plugin-async": "self.version", + "guzzle/plugin-backoff": "self.version", + "guzzle/plugin-cache": "self.version", + "guzzle/plugin-cookie": "self.version", + "guzzle/plugin-curlauth": "self.version", + "guzzle/plugin-error-response": "self.version", + "guzzle/plugin-history": "self.version", + "guzzle/plugin-log": "self.version", + "guzzle/plugin-md5": "self.version", + "guzzle/plugin-mock": "self.version", + "guzzle/plugin-oauth": "self.version", + "guzzle/service": "self.version", + "guzzle/stream": "self.version" }, - "suggest": { - "ext-curl": "*" + "require-dev": { + "doctrine/cache": "*", + "monolog/monolog": "1.*", + "phpunit/phpunit": "3.7.*", + "psr/log": "1.0.*", + "symfony/class-loader": "*", + "zendframework/zend-cache": "<2.3", + "zendframework/zend-log": "<2.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.7-dev" + "dev-master": "3.8-dev" } }, "autoload": { "psr-0": { - "Guzzle\\Http": "" + "Guzzle": "src/", + "Guzzle\\Tests": "tests/" } }, "notification-url": "https://packagist.org/downloads/", @@ -636,118 +620,25 @@ "name": "Michael Dowling", "email": "mtdowling@gmail.com", "homepage": "https://github.com/mtdowling" + }, + { + "name": "Guzzle Community", + "homepage": "https://github.com/guzzle/guzzle/contributors" } ], - "description": "HTTP libraries used by Guzzle", + "description": "Guzzle is a PHP HTTP client library and framework for building RESTful web service clients", "homepage": "http://guzzlephp.org/", "keywords": [ - "Guzzle", "client", "curl", + "framework", "http", - "http client" - ], - "abandoned": "guzzle/guzzle", - "time": "2014-01-23T18:23:29+00:00" - }, - { - "name": "guzzle/parser", - "version": "v3.8.1", - "target-dir": "Guzzle/Parser", - "source": { - "type": "git", - "url": "https://github.com/Guzzle3/parser.git", - "reference": "3f52387052f2e4ef083145a0f73c3654aa14e086" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Guzzle3/parser/zipball/3f52387052f2e4ef083145a0f73c3654aa14e086", - "reference": "3f52387052f2e4ef083145a0f73c3654aa14e086", - "shasum": "" - }, - "require": { - "php": ">=5.3.2" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.7-dev" - } - }, - "autoload": { - "psr-0": { - "Guzzle\\Parser": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Interchangeable parsers used by Guzzle", - "homepage": "http://guzzlephp.org/", - "keywords": [ - "URI Template", - "cookie", - "http", - "message", - "url" - ], - "abandoned": "guzzle/guzzle", - "time": "2013-10-24T00:04:09+00:00" - }, - { - "name": "guzzle/stream", - "version": "v3.8.1", - "target-dir": "Guzzle/Stream", - "source": { - "type": "git", - "url": "https://github.com/Guzzle3/stream.git", - "reference": "fa8af730ca714861c0001cfba64aaecc5f21bb96" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Guzzle3/stream/zipball/fa8af730ca714861c0001cfba64aaecc5f21bb96", - "reference": "fa8af730ca714861c0001cfba64aaecc5f21bb96", - "shasum": "" - }, - "require": { - "guzzle/common": "self.version", - "php": ">=5.3.2" - }, - "suggest": { - "guzzle/http": "To convert Guzzle request objects to PHP streams" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.7-dev" - } - }, - "autoload": { - "psr-0": { - "Guzzle\\Stream": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - } - ], - "description": "Guzzle stream wrapper component", - "homepage": "http://guzzlephp.org/", - "keywords": [ - "Guzzle", - "component", - "stream" + "http client", + "rest", + "web service" ], - "abandoned": "guzzle/guzzle", - "time": "2014-01-28T22:14:17+00:00" + "abandoned": "guzzlehttp/guzzle", + "time": "2014-01-28T22:29:15+00:00" }, { "name": "guzzlehttp/guzzle", @@ -1318,6 +1209,33 @@ ], "time": "2016-02-17T22:47:09+00:00" }, + { + "name": "mikemccabe/json-patch-php", + "version": "0.1.0", + "source": { + "type": "git", + "url": "https://github.com/mikemccabe/json-patch-php.git", + "reference": "b3af30a6aec7f6467c773cd49b2d974a70f7c0d4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mikemccabe/json-patch-php/zipball/b3af30a6aec7f6467c773cd49b2d974a70f7c0d4", + "reference": "b3af30a6aec7f6467c773cd49b2d974a70f7c0d4", + "shasum": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "mikemccabe\\JsonPatch\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0" + ], + "description": "Produce and apply json-patch objects", + "time": "2015-01-05T21:19:54+00:00" + }, { "name": "natxet/CssMin", "version": "dev-master", @@ -2023,45 +1941,45 @@ }, { "name": "rackspace/php-opencloud", - "version": "v1.9.2", + "version": "v1.16.0", "source": { "type": "git", "url": "https://github.com/rackspace/php-opencloud.git", - "reference": "6551de7aebcebb369d025662f99ab27c9b4527ac" + "reference": "d6b71feed7f9e7a4b52e0240a79f06473ba69c8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rackspace/php-opencloud/zipball/6551de7aebcebb369d025662f99ab27c9b4527ac", - "reference": "6551de7aebcebb369d025662f99ab27c9b4527ac", + "url": "https://api.github.com/repos/rackspace/php-opencloud/zipball/d6b71feed7f9e7a4b52e0240a79f06473ba69c8c", + "reference": "d6b71feed7f9e7a4b52e0240a79f06473ba69c8c", "shasum": "" }, "require": { - "guzzle/http": "3.8.*@dev", - "php": ">=5.3.3" + "guzzle/guzzle": "~3.8", + "mikemccabe/json-patch-php": "~0.1", + "php": ">=5.4", + "psr/log": "~1.0" }, "require-dev": { - "guzzle/guzzle": "dev-master", - "psr/log": "1.0.*", + "apigen/apigen": "~4.0", + "fabpot/php-cs-fixer": "1.0.*@dev", + "jakub-onderka/php-parallel-lint": "0.*", + "phpspec/prophecy": "~1.4", + "phpunit/phpunit": "4.3.*", "satooshi/php-coveralls": "0.6.*@dev" }, "type": "library", "autoload": { "psr-0": { "OpenCloud": [ - "lib/", - "tests/" + "lib/" ] } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "Apache-2.0" ], "authors": [ - { - "name": "Glen Campbell", - "email": "glen.campbell@rackspace.com" - }, { "name": "Jamie Hannaford", "email": "jamie.hannaford@rackspace.com", @@ -2076,7 +1994,7 @@ "rackspace", "swift" ], - "time": "2014-02-06T20:53:21+00:00" + "time": "2016-01-29T10:34:57+00:00" }, { "name": "react/promise", diff --git a/composer/autoload_classmap.php b/composer/autoload_classmap.php index f08050531..07739d91c 100644 --- a/composer/autoload_classmap.php +++ b/composer/autoload_classmap.php @@ -518,85 +518,412 @@ 'GuzzleHttp\\UriTemplate' => $vendorDir . '/guzzlehttp/guzzle/src/UriTemplate.php', 'GuzzleHttp\\Url' => $vendorDir . '/guzzlehttp/guzzle/src/Url.php', 'GuzzleHttp\\Utils' => $vendorDir . '/guzzlehttp/guzzle/src/Utils.php', - 'Guzzle\\Common\\AbstractHasDispatcher' => $vendorDir . '/guzzle/common/Guzzle/Common/AbstractHasDispatcher.php', - 'Guzzle\\Common\\Collection' => $vendorDir . '/guzzle/common/Guzzle/Common/Collection.php', - 'Guzzle\\Common\\Event' => $vendorDir . '/guzzle/common/Guzzle/Common/Event.php', - 'Guzzle\\Common\\Exception\\BadMethodCallException' => $vendorDir . '/guzzle/common/Guzzle/Common/Exception/BadMethodCallException.php', - 'Guzzle\\Common\\Exception\\ExceptionCollection' => $vendorDir . '/guzzle/common/Guzzle/Common/Exception/ExceptionCollection.php', - 'Guzzle\\Common\\Exception\\GuzzleException' => $vendorDir . '/guzzle/common/Guzzle/Common/Exception/GuzzleException.php', - 'Guzzle\\Common\\Exception\\InvalidArgumentException' => $vendorDir . '/guzzle/common/Guzzle/Common/Exception/InvalidArgumentException.php', - 'Guzzle\\Common\\Exception\\RuntimeException' => $vendorDir . '/guzzle/common/Guzzle/Common/Exception/RuntimeException.php', - 'Guzzle\\Common\\Exception\\UnexpectedValueException' => $vendorDir . '/guzzle/common/Guzzle/Common/Exception/UnexpectedValueException.php', - 'Guzzle\\Common\\FromConfigInterface' => $vendorDir . '/guzzle/common/Guzzle/Common/FromConfigInterface.php', - 'Guzzle\\Common\\HasDispatcherInterface' => $vendorDir . '/guzzle/common/Guzzle/Common/HasDispatcherInterface.php', - 'Guzzle\\Common\\ToArrayInterface' => $vendorDir . '/guzzle/common/Guzzle/Common/ToArrayInterface.php', - 'Guzzle\\Common\\Version' => $vendorDir . '/guzzle/common/Guzzle/Common/Version.php', - 'Guzzle\\Http\\AbstractEntityBodyDecorator' => $vendorDir . '/guzzle/http/Guzzle/Http/AbstractEntityBodyDecorator.php', - 'Guzzle\\Http\\CachingEntityBody' => $vendorDir . '/guzzle/http/Guzzle/Http/CachingEntityBody.php', - 'Guzzle\\Http\\Client' => $vendorDir . '/guzzle/http/Guzzle/Http/Client.php', - 'Guzzle\\Http\\ClientInterface' => $vendorDir . '/guzzle/http/Guzzle/Http/ClientInterface.php', - 'Guzzle\\Http\\Curl\\CurlHandle' => $vendorDir . '/guzzle/http/Guzzle/Http/Curl/CurlHandle.php', - 'Guzzle\\Http\\Curl\\CurlMulti' => $vendorDir . '/guzzle/http/Guzzle/Http/Curl/CurlMulti.php', - 'Guzzle\\Http\\Curl\\CurlMultiInterface' => $vendorDir . '/guzzle/http/Guzzle/Http/Curl/CurlMultiInterface.php', - 'Guzzle\\Http\\Curl\\CurlMultiProxy' => $vendorDir . '/guzzle/http/Guzzle/Http/Curl/CurlMultiProxy.php', - 'Guzzle\\Http\\Curl\\CurlVersion' => $vendorDir . '/guzzle/http/Guzzle/Http/Curl/CurlVersion.php', - 'Guzzle\\Http\\Curl\\RequestMediator' => $vendorDir . '/guzzle/http/Guzzle/Http/Curl/RequestMediator.php', - 'Guzzle\\Http\\EntityBody' => $vendorDir . '/guzzle/http/Guzzle/Http/EntityBody.php', - 'Guzzle\\Http\\EntityBodyInterface' => $vendorDir . '/guzzle/http/Guzzle/Http/EntityBodyInterface.php', - 'Guzzle\\Http\\Exception\\BadResponseException' => $vendorDir . '/guzzle/http/Guzzle/Http/Exception/BadResponseException.php', - 'Guzzle\\Http\\Exception\\ClientErrorResponseException' => $vendorDir . '/guzzle/http/Guzzle/Http/Exception/ClientErrorResponseException.php', - 'Guzzle\\Http\\Exception\\CouldNotRewindStreamException' => $vendorDir . '/guzzle/http/Guzzle/Http/Exception/CouldNotRewindStreamException.php', - 'Guzzle\\Http\\Exception\\CurlException' => $vendorDir . '/guzzle/http/Guzzle/Http/Exception/CurlException.php', - 'Guzzle\\Http\\Exception\\HttpException' => $vendorDir . '/guzzle/http/Guzzle/Http/Exception/HttpException.php', - 'Guzzle\\Http\\Exception\\MultiTransferException' => $vendorDir . '/guzzle/http/Guzzle/Http/Exception/MultiTransferException.php', - 'Guzzle\\Http\\Exception\\RequestException' => $vendorDir . '/guzzle/http/Guzzle/Http/Exception/RequestException.php', - 'Guzzle\\Http\\Exception\\ServerErrorResponseException' => $vendorDir . '/guzzle/http/Guzzle/Http/Exception/ServerErrorResponseException.php', - 'Guzzle\\Http\\Exception\\TooManyRedirectsException' => $vendorDir . '/guzzle/http/Guzzle/Http/Exception/TooManyRedirectsException.php', - 'Guzzle\\Http\\IoEmittingEntityBody' => $vendorDir . '/guzzle/http/Guzzle/Http/IoEmittingEntityBody.php', - 'Guzzle\\Http\\Message\\AbstractMessage' => $vendorDir . '/guzzle/http/Guzzle/Http/Message/AbstractMessage.php', - 'Guzzle\\Http\\Message\\EntityEnclosingRequest' => $vendorDir . '/guzzle/http/Guzzle/Http/Message/EntityEnclosingRequest.php', - 'Guzzle\\Http\\Message\\EntityEnclosingRequestInterface' => $vendorDir . '/guzzle/http/Guzzle/Http/Message/EntityEnclosingRequestInterface.php', - 'Guzzle\\Http\\Message\\Header' => $vendorDir . '/guzzle/http/Guzzle/Http/Message/Header.php', - 'Guzzle\\Http\\Message\\Header\\CacheControl' => $vendorDir . '/guzzle/http/Guzzle/Http/Message/Header/CacheControl.php', - 'Guzzle\\Http\\Message\\Header\\HeaderCollection' => $vendorDir . '/guzzle/http/Guzzle/Http/Message/Header/HeaderCollection.php', - 'Guzzle\\Http\\Message\\Header\\HeaderFactory' => $vendorDir . '/guzzle/http/Guzzle/Http/Message/Header/HeaderFactory.php', - 'Guzzle\\Http\\Message\\Header\\HeaderFactoryInterface' => $vendorDir . '/guzzle/http/Guzzle/Http/Message/Header/HeaderFactoryInterface.php', - 'Guzzle\\Http\\Message\\Header\\HeaderInterface' => $vendorDir . '/guzzle/http/Guzzle/Http/Message/Header/HeaderInterface.php', - 'Guzzle\\Http\\Message\\Header\\Link' => $vendorDir . '/guzzle/http/Guzzle/Http/Message/Header/Link.php', - 'Guzzle\\Http\\Message\\MessageInterface' => $vendorDir . '/guzzle/http/Guzzle/Http/Message/MessageInterface.php', - 'Guzzle\\Http\\Message\\PostFile' => $vendorDir . '/guzzle/http/Guzzle/Http/Message/PostFile.php', - 'Guzzle\\Http\\Message\\PostFileInterface' => $vendorDir . '/guzzle/http/Guzzle/Http/Message/PostFileInterface.php', - 'Guzzle\\Http\\Message\\Request' => $vendorDir . '/guzzle/http/Guzzle/Http/Message/Request.php', - 'Guzzle\\Http\\Message\\RequestFactory' => $vendorDir . '/guzzle/http/Guzzle/Http/Message/RequestFactory.php', - 'Guzzle\\Http\\Message\\RequestFactoryInterface' => $vendorDir . '/guzzle/http/Guzzle/Http/Message/RequestFactoryInterface.php', - 'Guzzle\\Http\\Message\\RequestInterface' => $vendorDir . '/guzzle/http/Guzzle/Http/Message/RequestInterface.php', - 'Guzzle\\Http\\Message\\Response' => $vendorDir . '/guzzle/http/Guzzle/Http/Message/Response.php', - 'Guzzle\\Http\\Mimetypes' => $vendorDir . '/guzzle/http/Guzzle/Http/Mimetypes.php', - 'Guzzle\\Http\\QueryAggregator\\CommaAggregator' => $vendorDir . '/guzzle/http/Guzzle/Http/QueryAggregator/CommaAggregator.php', - 'Guzzle\\Http\\QueryAggregator\\DuplicateAggregator' => $vendorDir . '/guzzle/http/Guzzle/Http/QueryAggregator/DuplicateAggregator.php', - 'Guzzle\\Http\\QueryAggregator\\PhpAggregator' => $vendorDir . '/guzzle/http/Guzzle/Http/QueryAggregator/PhpAggregator.php', - 'Guzzle\\Http\\QueryAggregator\\QueryAggregatorInterface' => $vendorDir . '/guzzle/http/Guzzle/Http/QueryAggregator/QueryAggregatorInterface.php', - 'Guzzle\\Http\\QueryString' => $vendorDir . '/guzzle/http/Guzzle/Http/QueryString.php', - 'Guzzle\\Http\\ReadLimitEntityBody' => $vendorDir . '/guzzle/http/Guzzle/Http/ReadLimitEntityBody.php', - 'Guzzle\\Http\\RedirectPlugin' => $vendorDir . '/guzzle/http/Guzzle/Http/RedirectPlugin.php', - 'Guzzle\\Http\\StaticClient' => $vendorDir . '/guzzle/http/Guzzle/Http/StaticClient.php', - 'Guzzle\\Http\\Url' => $vendorDir . '/guzzle/http/Guzzle/Http/Url.php', - 'Guzzle\\Parser\\Cookie\\CookieParser' => $vendorDir . '/guzzle/parser/Guzzle/Parser/Cookie/CookieParser.php', - 'Guzzle\\Parser\\Cookie\\CookieParserInterface' => $vendorDir . '/guzzle/parser/Guzzle/Parser/Cookie/CookieParserInterface.php', - 'Guzzle\\Parser\\Message\\AbstractMessageParser' => $vendorDir . '/guzzle/parser/Guzzle/Parser/Message/AbstractMessageParser.php', - 'Guzzle\\Parser\\Message\\MessageParser' => $vendorDir . '/guzzle/parser/Guzzle/Parser/Message/MessageParser.php', - 'Guzzle\\Parser\\Message\\MessageParserInterface' => $vendorDir . '/guzzle/parser/Guzzle/Parser/Message/MessageParserInterface.php', - 'Guzzle\\Parser\\Message\\PeclHttpMessageParser' => $vendorDir . '/guzzle/parser/Guzzle/Parser/Message/PeclHttpMessageParser.php', - 'Guzzle\\Parser\\ParserRegistry' => $vendorDir . '/guzzle/parser/Guzzle/Parser/ParserRegistry.php', - 'Guzzle\\Parser\\UriTemplate\\PeclUriTemplate' => $vendorDir . '/guzzle/parser/Guzzle/Parser/UriTemplate/PeclUriTemplate.php', - 'Guzzle\\Parser\\UriTemplate\\UriTemplate' => $vendorDir . '/guzzle/parser/Guzzle/Parser/UriTemplate/UriTemplate.php', - 'Guzzle\\Parser\\UriTemplate\\UriTemplateInterface' => $vendorDir . '/guzzle/parser/Guzzle/Parser/UriTemplate/UriTemplateInterface.php', - 'Guzzle\\Parser\\Url\\UrlParser' => $vendorDir . '/guzzle/parser/Guzzle/Parser/Url/UrlParser.php', - 'Guzzle\\Parser\\Url\\UrlParserInterface' => $vendorDir . '/guzzle/parser/Guzzle/Parser/Url/UrlParserInterface.php', - 'Guzzle\\Stream\\PhpStreamRequestFactory' => $vendorDir . '/guzzle/stream/Guzzle/Stream/PhpStreamRequestFactory.php', - 'Guzzle\\Stream\\Stream' => $vendorDir . '/guzzle/stream/Guzzle/Stream/Stream.php', - 'Guzzle\\Stream\\StreamInterface' => $vendorDir . '/guzzle/stream/Guzzle/Stream/StreamInterface.php', - 'Guzzle\\Stream\\StreamRequestFactoryInterface' => $vendorDir . '/guzzle/stream/Guzzle/Stream/StreamRequestFactoryInterface.php', + 'Guzzle\\Batch\\AbstractBatchDecorator' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Batch/AbstractBatchDecorator.php', + 'Guzzle\\Batch\\Batch' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Batch/Batch.php', + 'Guzzle\\Batch\\BatchBuilder' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Batch/BatchBuilder.php', + 'Guzzle\\Batch\\BatchClosureDivisor' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Batch/BatchClosureDivisor.php', + 'Guzzle\\Batch\\BatchClosureTransfer' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Batch/BatchClosureTransfer.php', + 'Guzzle\\Batch\\BatchCommandTransfer' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Batch/BatchCommandTransfer.php', + 'Guzzle\\Batch\\BatchDivisorInterface' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Batch/BatchDivisorInterface.php', + 'Guzzle\\Batch\\BatchInterface' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Batch/BatchInterface.php', + 'Guzzle\\Batch\\BatchRequestTransfer' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Batch/BatchRequestTransfer.php', + 'Guzzle\\Batch\\BatchSizeDivisor' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Batch/BatchSizeDivisor.php', + 'Guzzle\\Batch\\BatchTransferInterface' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Batch/BatchTransferInterface.php', + 'Guzzle\\Batch\\ExceptionBufferingBatch' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Batch/ExceptionBufferingBatch.php', + 'Guzzle\\Batch\\Exception\\BatchTransferException' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Batch/Exception/BatchTransferException.php', + 'Guzzle\\Batch\\FlushingBatch' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Batch/FlushingBatch.php', + 'Guzzle\\Batch\\HistoryBatch' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Batch/HistoryBatch.php', + 'Guzzle\\Batch\\NotifyingBatch' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Batch/NotifyingBatch.php', + 'Guzzle\\Cache\\AbstractCacheAdapter' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Cache/AbstractCacheAdapter.php', + 'Guzzle\\Cache\\CacheAdapterFactory' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Cache/CacheAdapterFactory.php', + 'Guzzle\\Cache\\CacheAdapterInterface' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Cache/CacheAdapterInterface.php', + 'Guzzle\\Cache\\ClosureCacheAdapter' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Cache/ClosureCacheAdapter.php', + 'Guzzle\\Cache\\DoctrineCacheAdapter' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Cache/DoctrineCacheAdapter.php', + 'Guzzle\\Cache\\NullCacheAdapter' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Cache/NullCacheAdapter.php', + 'Guzzle\\Cache\\Zf1CacheAdapter' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Cache/Zf1CacheAdapter.php', + 'Guzzle\\Cache\\Zf2CacheAdapter' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Cache/Zf2CacheAdapter.php', + 'Guzzle\\Common\\AbstractHasDispatcher' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Common/AbstractHasDispatcher.php', + 'Guzzle\\Common\\Collection' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Common/Collection.php', + 'Guzzle\\Common\\Event' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Common/Event.php', + 'Guzzle\\Common\\Exception\\BadMethodCallException' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Common/Exception/BadMethodCallException.php', + 'Guzzle\\Common\\Exception\\ExceptionCollection' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Common/Exception/ExceptionCollection.php', + 'Guzzle\\Common\\Exception\\GuzzleException' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Common/Exception/GuzzleException.php', + 'Guzzle\\Common\\Exception\\InvalidArgumentException' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Common/Exception/InvalidArgumentException.php', + 'Guzzle\\Common\\Exception\\RuntimeException' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Common/Exception/RuntimeException.php', + 'Guzzle\\Common\\Exception\\UnexpectedValueException' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Common/Exception/UnexpectedValueException.php', + 'Guzzle\\Common\\FromConfigInterface' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Common/FromConfigInterface.php', + 'Guzzle\\Common\\HasDispatcherInterface' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Common/HasDispatcherInterface.php', + 'Guzzle\\Common\\ToArrayInterface' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Common/ToArrayInterface.php', + 'Guzzle\\Common\\Version' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Common/Version.php', + 'Guzzle\\Http\\AbstractEntityBodyDecorator' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Http/AbstractEntityBodyDecorator.php', + 'Guzzle\\Http\\CachingEntityBody' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Http/CachingEntityBody.php', + 'Guzzle\\Http\\Client' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Http/Client.php', + 'Guzzle\\Http\\ClientInterface' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Http/ClientInterface.php', + 'Guzzle\\Http\\Curl\\CurlHandle' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Http/Curl/CurlHandle.php', + 'Guzzle\\Http\\Curl\\CurlMulti' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Http/Curl/CurlMulti.php', + 'Guzzle\\Http\\Curl\\CurlMultiInterface' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Http/Curl/CurlMultiInterface.php', + 'Guzzle\\Http\\Curl\\CurlMultiProxy' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Http/Curl/CurlMultiProxy.php', + 'Guzzle\\Http\\Curl\\CurlVersion' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Http/Curl/CurlVersion.php', + 'Guzzle\\Http\\Curl\\RequestMediator' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Http/Curl/RequestMediator.php', + 'Guzzle\\Http\\EntityBody' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Http/EntityBody.php', + 'Guzzle\\Http\\EntityBodyInterface' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Http/EntityBodyInterface.php', + 'Guzzle\\Http\\Exception\\BadResponseException' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Http/Exception/BadResponseException.php', + 'Guzzle\\Http\\Exception\\ClientErrorResponseException' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Http/Exception/ClientErrorResponseException.php', + 'Guzzle\\Http\\Exception\\CouldNotRewindStreamException' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Http/Exception/CouldNotRewindStreamException.php', + 'Guzzle\\Http\\Exception\\CurlException' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Http/Exception/CurlException.php', + 'Guzzle\\Http\\Exception\\HttpException' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Http/Exception/HttpException.php', + 'Guzzle\\Http\\Exception\\MultiTransferException' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Http/Exception/MultiTransferException.php', + 'Guzzle\\Http\\Exception\\RequestException' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Http/Exception/RequestException.php', + 'Guzzle\\Http\\Exception\\ServerErrorResponseException' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Http/Exception/ServerErrorResponseException.php', + 'Guzzle\\Http\\Exception\\TooManyRedirectsException' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Http/Exception/TooManyRedirectsException.php', + 'Guzzle\\Http\\IoEmittingEntityBody' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Http/IoEmittingEntityBody.php', + 'Guzzle\\Http\\Message\\AbstractMessage' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Http/Message/AbstractMessage.php', + 'Guzzle\\Http\\Message\\EntityEnclosingRequest' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Http/Message/EntityEnclosingRequest.php', + 'Guzzle\\Http\\Message\\EntityEnclosingRequestInterface' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Http/Message/EntityEnclosingRequestInterface.php', + 'Guzzle\\Http\\Message\\Header' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Http/Message/Header.php', + 'Guzzle\\Http\\Message\\Header\\CacheControl' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Http/Message/Header/CacheControl.php', + 'Guzzle\\Http\\Message\\Header\\HeaderCollection' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Http/Message/Header/HeaderCollection.php', + 'Guzzle\\Http\\Message\\Header\\HeaderFactory' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Http/Message/Header/HeaderFactory.php', + 'Guzzle\\Http\\Message\\Header\\HeaderFactoryInterface' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Http/Message/Header/HeaderFactoryInterface.php', + 'Guzzle\\Http\\Message\\Header\\HeaderInterface' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Http/Message/Header/HeaderInterface.php', + 'Guzzle\\Http\\Message\\Header\\Link' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Http/Message/Header/Link.php', + 'Guzzle\\Http\\Message\\MessageInterface' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Http/Message/MessageInterface.php', + 'Guzzle\\Http\\Message\\PostFile' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Http/Message/PostFile.php', + 'Guzzle\\Http\\Message\\PostFileInterface' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Http/Message/PostFileInterface.php', + 'Guzzle\\Http\\Message\\Request' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Http/Message/Request.php', + 'Guzzle\\Http\\Message\\RequestFactory' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Http/Message/RequestFactory.php', + 'Guzzle\\Http\\Message\\RequestFactoryInterface' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Http/Message/RequestFactoryInterface.php', + 'Guzzle\\Http\\Message\\RequestInterface' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Http/Message/RequestInterface.php', + 'Guzzle\\Http\\Message\\Response' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Http/Message/Response.php', + 'Guzzle\\Http\\Mimetypes' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Http/Mimetypes.php', + 'Guzzle\\Http\\QueryAggregator\\CommaAggregator' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Http/QueryAggregator/CommaAggregator.php', + 'Guzzle\\Http\\QueryAggregator\\DuplicateAggregator' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Http/QueryAggregator/DuplicateAggregator.php', + 'Guzzle\\Http\\QueryAggregator\\PhpAggregator' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Http/QueryAggregator/PhpAggregator.php', + 'Guzzle\\Http\\QueryAggregator\\QueryAggregatorInterface' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Http/QueryAggregator/QueryAggregatorInterface.php', + 'Guzzle\\Http\\QueryString' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Http/QueryString.php', + 'Guzzle\\Http\\ReadLimitEntityBody' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Http/ReadLimitEntityBody.php', + 'Guzzle\\Http\\RedirectPlugin' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Http/RedirectPlugin.php', + 'Guzzle\\Http\\StaticClient' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Http/StaticClient.php', + 'Guzzle\\Http\\Url' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Http/Url.php', + 'Guzzle\\Inflection\\Inflector' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Inflection/Inflector.php', + 'Guzzle\\Inflection\\InflectorInterface' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Inflection/InflectorInterface.php', + 'Guzzle\\Inflection\\MemoizingInflector' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Inflection/MemoizingInflector.php', + 'Guzzle\\Inflection\\PreComputedInflector' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Inflection/PreComputedInflector.php', + 'Guzzle\\Iterator\\AppendIterator' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Iterator/AppendIterator.php', + 'Guzzle\\Iterator\\ChunkedIterator' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Iterator/ChunkedIterator.php', + 'Guzzle\\Iterator\\FilterIterator' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Iterator/FilterIterator.php', + 'Guzzle\\Iterator\\MapIterator' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Iterator/MapIterator.php', + 'Guzzle\\Iterator\\MethodProxyIterator' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Iterator/MethodProxyIterator.php', + 'Guzzle\\Log\\AbstractLogAdapter' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Log/AbstractLogAdapter.php', + 'Guzzle\\Log\\ArrayLogAdapter' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Log/ArrayLogAdapter.php', + 'Guzzle\\Log\\ClosureLogAdapter' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Log/ClosureLogAdapter.php', + 'Guzzle\\Log\\LogAdapterInterface' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Log/LogAdapterInterface.php', + 'Guzzle\\Log\\MessageFormatter' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Log/MessageFormatter.php', + 'Guzzle\\Log\\MonologLogAdapter' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Log/MonologLogAdapter.php', + 'Guzzle\\Log\\PsrLogAdapter' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Log/PsrLogAdapter.php', + 'Guzzle\\Log\\Zf1LogAdapter' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Log/Zf1LogAdapter.php', + 'Guzzle\\Log\\Zf2LogAdapter' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Log/Zf2LogAdapter.php', + 'Guzzle\\Parser\\Cookie\\CookieParser' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Parser/Cookie/CookieParser.php', + 'Guzzle\\Parser\\Cookie\\CookieParserInterface' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Parser/Cookie/CookieParserInterface.php', + 'Guzzle\\Parser\\Message\\AbstractMessageParser' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Parser/Message/AbstractMessageParser.php', + 'Guzzle\\Parser\\Message\\MessageParser' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Parser/Message/MessageParser.php', + 'Guzzle\\Parser\\Message\\MessageParserInterface' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Parser/Message/MessageParserInterface.php', + 'Guzzle\\Parser\\Message\\PeclHttpMessageParser' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Parser/Message/PeclHttpMessageParser.php', + 'Guzzle\\Parser\\ParserRegistry' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Parser/ParserRegistry.php', + 'Guzzle\\Parser\\UriTemplate\\PeclUriTemplate' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Parser/UriTemplate/PeclUriTemplate.php', + 'Guzzle\\Parser\\UriTemplate\\UriTemplate' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Parser/UriTemplate/UriTemplate.php', + 'Guzzle\\Parser\\UriTemplate\\UriTemplateInterface' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Parser/UriTemplate/UriTemplateInterface.php', + 'Guzzle\\Parser\\Url\\UrlParser' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Parser/Url/UrlParser.php', + 'Guzzle\\Parser\\Url\\UrlParserInterface' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Parser/Url/UrlParserInterface.php', + 'Guzzle\\Plugin\\Async\\AsyncPlugin' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Plugin/Async/AsyncPlugin.php', + 'Guzzle\\Plugin\\Backoff\\AbstractBackoffStrategy' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Plugin/Backoff/AbstractBackoffStrategy.php', + 'Guzzle\\Plugin\\Backoff\\AbstractErrorCodeBackoffStrategy' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Plugin/Backoff/AbstractErrorCodeBackoffStrategy.php', + 'Guzzle\\Plugin\\Backoff\\BackoffLogger' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Plugin/Backoff/BackoffLogger.php', + 'Guzzle\\Plugin\\Backoff\\BackoffPlugin' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Plugin/Backoff/BackoffPlugin.php', + 'Guzzle\\Plugin\\Backoff\\BackoffStrategyInterface' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Plugin/Backoff/BackoffStrategyInterface.php', + 'Guzzle\\Plugin\\Backoff\\CallbackBackoffStrategy' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Plugin/Backoff/CallbackBackoffStrategy.php', + 'Guzzle\\Plugin\\Backoff\\ConstantBackoffStrategy' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Plugin/Backoff/ConstantBackoffStrategy.php', + 'Guzzle\\Plugin\\Backoff\\CurlBackoffStrategy' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Plugin/Backoff/CurlBackoffStrategy.php', + 'Guzzle\\Plugin\\Backoff\\ExponentialBackoffStrategy' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Plugin/Backoff/ExponentialBackoffStrategy.php', + 'Guzzle\\Plugin\\Backoff\\HttpBackoffStrategy' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Plugin/Backoff/HttpBackoffStrategy.php', + 'Guzzle\\Plugin\\Backoff\\LinearBackoffStrategy' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Plugin/Backoff/LinearBackoffStrategy.php', + 'Guzzle\\Plugin\\Backoff\\ReasonPhraseBackoffStrategy' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Plugin/Backoff/ReasonPhraseBackoffStrategy.php', + 'Guzzle\\Plugin\\Backoff\\TruncatedBackoffStrategy' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Plugin/Backoff/TruncatedBackoffStrategy.php', + 'Guzzle\\Plugin\\Cache\\CacheKeyProviderInterface' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Plugin/Cache/CacheKeyProviderInterface.php', + 'Guzzle\\Plugin\\Cache\\CachePlugin' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Plugin/Cache/CachePlugin.php', + 'Guzzle\\Plugin\\Cache\\CacheStorageInterface' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Plugin/Cache/CacheStorageInterface.php', + 'Guzzle\\Plugin\\Cache\\CallbackCanCacheStrategy' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Plugin/Cache/CallbackCanCacheStrategy.php', + 'Guzzle\\Plugin\\Cache\\CanCacheStrategyInterface' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Plugin/Cache/CanCacheStrategyInterface.php', + 'Guzzle\\Plugin\\Cache\\DefaultCacheKeyProvider' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Plugin/Cache/DefaultCacheKeyProvider.php', + 'Guzzle\\Plugin\\Cache\\DefaultCacheStorage' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Plugin/Cache/DefaultCacheStorage.php', + 'Guzzle\\Plugin\\Cache\\DefaultCanCacheStrategy' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Plugin/Cache/DefaultCanCacheStrategy.php', + 'Guzzle\\Plugin\\Cache\\DefaultRevalidation' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Plugin/Cache/DefaultRevalidation.php', + 'Guzzle\\Plugin\\Cache\\DenyRevalidation' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Plugin/Cache/DenyRevalidation.php', + 'Guzzle\\Plugin\\Cache\\RevalidationInterface' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Plugin/Cache/RevalidationInterface.php', + 'Guzzle\\Plugin\\Cache\\SkipRevalidation' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Plugin/Cache/SkipRevalidation.php', + 'Guzzle\\Plugin\\Cookie\\Cookie' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Plugin/Cookie/Cookie.php', + 'Guzzle\\Plugin\\Cookie\\CookieJar\\ArrayCookieJar' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Plugin/Cookie/CookieJar/ArrayCookieJar.php', + 'Guzzle\\Plugin\\Cookie\\CookieJar\\CookieJarInterface' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Plugin/Cookie/CookieJar/CookieJarInterface.php', + 'Guzzle\\Plugin\\Cookie\\CookieJar\\FileCookieJar' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Plugin/Cookie/CookieJar/FileCookieJar.php', + 'Guzzle\\Plugin\\Cookie\\CookiePlugin' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Plugin/Cookie/CookiePlugin.php', + 'Guzzle\\Plugin\\Cookie\\Exception\\InvalidCookieException' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Plugin/Cookie/Exception/InvalidCookieException.php', + 'Guzzle\\Plugin\\CurlAuth\\CurlAuthPlugin' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Plugin/CurlAuth/CurlAuthPlugin.php', + 'Guzzle\\Plugin\\ErrorResponse\\ErrorResponseExceptionInterface' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Plugin/ErrorResponse/ErrorResponseExceptionInterface.php', + 'Guzzle\\Plugin\\ErrorResponse\\ErrorResponsePlugin' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Plugin/ErrorResponse/ErrorResponsePlugin.php', + 'Guzzle\\Plugin\\ErrorResponse\\Exception\\ErrorResponseException' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Plugin/ErrorResponse/Exception/ErrorResponseException.php', + 'Guzzle\\Plugin\\History\\HistoryPlugin' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Plugin/History/HistoryPlugin.php', + 'Guzzle\\Plugin\\Log\\LogPlugin' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Plugin/Log/LogPlugin.php', + 'Guzzle\\Plugin\\Md5\\CommandContentMd5Plugin' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Plugin/Md5/CommandContentMd5Plugin.php', + 'Guzzle\\Plugin\\Md5\\Md5ValidatorPlugin' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Plugin/Md5/Md5ValidatorPlugin.php', + 'Guzzle\\Plugin\\Mock\\MockPlugin' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Plugin/Mock/MockPlugin.php', + 'Guzzle\\Plugin\\Oauth\\OauthPlugin' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Plugin/Oauth/OauthPlugin.php', + 'Guzzle\\Service\\AbstractConfigLoader' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/AbstractConfigLoader.php', + 'Guzzle\\Service\\Builder\\ServiceBuilder' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Builder/ServiceBuilder.php', + 'Guzzle\\Service\\Builder\\ServiceBuilderInterface' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Builder/ServiceBuilderInterface.php', + 'Guzzle\\Service\\Builder\\ServiceBuilderLoader' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Builder/ServiceBuilderLoader.php', + 'Guzzle\\Service\\CachingConfigLoader' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/CachingConfigLoader.php', + 'Guzzle\\Service\\Client' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Client.php', + 'Guzzle\\Service\\ClientInterface' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/ClientInterface.php', + 'Guzzle\\Service\\Command\\AbstractCommand' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Command/AbstractCommand.php', + 'Guzzle\\Service\\Command\\ClosureCommand' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Command/ClosureCommand.php', + 'Guzzle\\Service\\Command\\CommandInterface' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Command/CommandInterface.php', + 'Guzzle\\Service\\Command\\CreateResponseClassEvent' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Command/CreateResponseClassEvent.php', + 'Guzzle\\Service\\Command\\DefaultRequestSerializer' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Command/DefaultRequestSerializer.php', + 'Guzzle\\Service\\Command\\DefaultResponseParser' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Command/DefaultResponseParser.php', + 'Guzzle\\Service\\Command\\Factory\\AliasFactory' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Command/Factory/AliasFactory.php', + 'Guzzle\\Service\\Command\\Factory\\CompositeFactory' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Command/Factory/CompositeFactory.php', + 'Guzzle\\Service\\Command\\Factory\\ConcreteClassFactory' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Command/Factory/ConcreteClassFactory.php', + 'Guzzle\\Service\\Command\\Factory\\FactoryInterface' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Command/Factory/FactoryInterface.php', + 'Guzzle\\Service\\Command\\Factory\\MapFactory' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Command/Factory/MapFactory.php', + 'Guzzle\\Service\\Command\\Factory\\ServiceDescriptionFactory' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Command/Factory/ServiceDescriptionFactory.php', + 'Guzzle\\Service\\Command\\LocationVisitor\\Request\\AbstractRequestVisitor' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/AbstractRequestVisitor.php', + 'Guzzle\\Service\\Command\\LocationVisitor\\Request\\BodyVisitor' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/BodyVisitor.php', + 'Guzzle\\Service\\Command\\LocationVisitor\\Request\\HeaderVisitor' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/HeaderVisitor.php', + 'Guzzle\\Service\\Command\\LocationVisitor\\Request\\JsonVisitor' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/JsonVisitor.php', + 'Guzzle\\Service\\Command\\LocationVisitor\\Request\\PostFieldVisitor' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/PostFieldVisitor.php', + 'Guzzle\\Service\\Command\\LocationVisitor\\Request\\PostFileVisitor' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/PostFileVisitor.php', + 'Guzzle\\Service\\Command\\LocationVisitor\\Request\\QueryVisitor' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/QueryVisitor.php', + 'Guzzle\\Service\\Command\\LocationVisitor\\Request\\RequestVisitorInterface' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/RequestVisitorInterface.php', + 'Guzzle\\Service\\Command\\LocationVisitor\\Request\\ResponseBodyVisitor' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/ResponseBodyVisitor.php', + 'Guzzle\\Service\\Command\\LocationVisitor\\Request\\XmlVisitor' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/XmlVisitor.php', + 'Guzzle\\Service\\Command\\LocationVisitor\\Response\\AbstractResponseVisitor' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Response/AbstractResponseVisitor.php', + 'Guzzle\\Service\\Command\\LocationVisitor\\Response\\BodyVisitor' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Response/BodyVisitor.php', + 'Guzzle\\Service\\Command\\LocationVisitor\\Response\\HeaderVisitor' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Response/HeaderVisitor.php', + 'Guzzle\\Service\\Command\\LocationVisitor\\Response\\JsonVisitor' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Response/JsonVisitor.php', + 'Guzzle\\Service\\Command\\LocationVisitor\\Response\\ReasonPhraseVisitor' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Response/ReasonPhraseVisitor.php', + 'Guzzle\\Service\\Command\\LocationVisitor\\Response\\ResponseVisitorInterface' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Response/ResponseVisitorInterface.php', + 'Guzzle\\Service\\Command\\LocationVisitor\\Response\\StatusCodeVisitor' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Response/StatusCodeVisitor.php', + 'Guzzle\\Service\\Command\\LocationVisitor\\Response\\XmlVisitor' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Response/XmlVisitor.php', + 'Guzzle\\Service\\Command\\LocationVisitor\\VisitorFlyweight' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/VisitorFlyweight.php', + 'Guzzle\\Service\\Command\\OperationCommand' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Command/OperationCommand.php', + 'Guzzle\\Service\\Command\\OperationResponseParser' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Command/OperationResponseParser.php', + 'Guzzle\\Service\\Command\\RequestSerializerInterface' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Command/RequestSerializerInterface.php', + 'Guzzle\\Service\\Command\\ResponseClassInterface' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Command/ResponseClassInterface.php', + 'Guzzle\\Service\\Command\\ResponseParserInterface' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Command/ResponseParserInterface.php', + 'Guzzle\\Service\\ConfigLoaderInterface' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/ConfigLoaderInterface.php', + 'Guzzle\\Service\\Description\\Operation' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Description/Operation.php', + 'Guzzle\\Service\\Description\\OperationInterface' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Description/OperationInterface.php', + 'Guzzle\\Service\\Description\\Parameter' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Description/Parameter.php', + 'Guzzle\\Service\\Description\\SchemaFormatter' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Description/SchemaFormatter.php', + 'Guzzle\\Service\\Description\\SchemaValidator' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Description/SchemaValidator.php', + 'Guzzle\\Service\\Description\\ServiceDescription' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Description/ServiceDescription.php', + 'Guzzle\\Service\\Description\\ServiceDescriptionInterface' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Description/ServiceDescriptionInterface.php', + 'Guzzle\\Service\\Description\\ServiceDescriptionLoader' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Description/ServiceDescriptionLoader.php', + 'Guzzle\\Service\\Description\\ValidatorInterface' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Description/ValidatorInterface.php', + 'Guzzle\\Service\\Exception\\CommandException' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Exception/CommandException.php', + 'Guzzle\\Service\\Exception\\CommandTransferException' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Exception/CommandTransferException.php', + 'Guzzle\\Service\\Exception\\DescriptionBuilderException' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Exception/DescriptionBuilderException.php', + 'Guzzle\\Service\\Exception\\InconsistentClientTransferException' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Exception/InconsistentClientTransferException.php', + 'Guzzle\\Service\\Exception\\ResponseClassException' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Exception/ResponseClassException.php', + 'Guzzle\\Service\\Exception\\ServiceBuilderException' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Exception/ServiceBuilderException.php', + 'Guzzle\\Service\\Exception\\ServiceNotFoundException' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Exception/ServiceNotFoundException.php', + 'Guzzle\\Service\\Exception\\ValidationException' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Exception/ValidationException.php', + 'Guzzle\\Service\\Resource\\AbstractResourceIteratorFactory' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Resource/AbstractResourceIteratorFactory.php', + 'Guzzle\\Service\\Resource\\CompositeResourceIteratorFactory' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Resource/CompositeResourceIteratorFactory.php', + 'Guzzle\\Service\\Resource\\MapResourceIteratorFactory' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Resource/MapResourceIteratorFactory.php', + 'Guzzle\\Service\\Resource\\Model' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Resource/Model.php', + 'Guzzle\\Service\\Resource\\ResourceIterator' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Resource/ResourceIterator.php', + 'Guzzle\\Service\\Resource\\ResourceIteratorApplyBatched' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Resource/ResourceIteratorApplyBatched.php', + 'Guzzle\\Service\\Resource\\ResourceIteratorClassFactory' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Resource/ResourceIteratorClassFactory.php', + 'Guzzle\\Service\\Resource\\ResourceIteratorFactoryInterface' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Resource/ResourceIteratorFactoryInterface.php', + 'Guzzle\\Service\\Resource\\ResourceIteratorInterface' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Service/Resource/ResourceIteratorInterface.php', + 'Guzzle\\Stream\\PhpStreamRequestFactory' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Stream/PhpStreamRequestFactory.php', + 'Guzzle\\Stream\\Stream' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Stream/Stream.php', + 'Guzzle\\Stream\\StreamInterface' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Stream/StreamInterface.php', + 'Guzzle\\Stream\\StreamRequestFactoryInterface' => $vendorDir . '/guzzle/guzzle/src/Guzzle/Stream/StreamRequestFactoryInterface.php', + 'Guzzle\\Tests\\Batch\\AbstractBatchDecoratorTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Batch/AbstractBatchDecoratorTest.php', + 'Guzzle\\Tests\\Batch\\BatchBuilderTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Batch/BatchBuilderTest.php', + 'Guzzle\\Tests\\Batch\\BatchClosureDivisorTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Batch/BatchClosureDivisorTest.php', + 'Guzzle\\Tests\\Batch\\BatchClosureTransferTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Batch/BatchClosureTransferTest.php', + 'Guzzle\\Tests\\Batch\\BatchCommandTransferTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Batch/BatchCommandTransferTest.php', + 'Guzzle\\Tests\\Batch\\BatchRequestTransferTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Batch/BatchRequestTransferTest.php', + 'Guzzle\\Tests\\Batch\\BatchSizeDivisorTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Batch/BatchSizeDivisorTest.php', + 'Guzzle\\Tests\\Batch\\BatchTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Batch/BatchTest.php', + 'Guzzle\\Tests\\Batch\\ExceptionBufferingBatchTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Batch/ExceptionBufferingBatchTest.php', + 'Guzzle\\Tests\\Batch\\FlushingBatchTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Batch/FlushingBatchTest.php', + 'Guzzle\\Tests\\Batch\\HistoryBatchTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Batch/HistoryBatchTest.php', + 'Guzzle\\Tests\\Batch\\NotifyingBatchTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Batch/NotifyingBatchTest.php', + 'Guzzle\\Tests\\Cache\\CacheAdapterFactoryTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Cache/CacheAdapterFactoryTest.php', + 'Guzzle\\Tests\\Cache\\CacheAdapterTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Cache/CacheAdapterTest.php', + 'Guzzle\\Tests\\Cache\\ClosureCacheAdapterTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Cache/ClosureCacheAdapterTest.php', + 'Guzzle\\Tests\\Cache\\Zf2CacheAdapterTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Cache/Zf2CacheAdapterTest.php', + 'Guzzle\\Tests\\Common\\AbstractHasAdapterTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Common/AbstractHasDispatcherTest.php', + 'Guzzle\\Tests\\Common\\Cache\\NullCacheAdapterTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Cache/NullCacheAdapterTest.php', + 'Guzzle\\Tests\\Common\\CollectionTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Common/CollectionTest.php', + 'Guzzle\\Tests\\Common\\EventTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Common/EventTest.php', + 'Guzzle\\Tests\\Common\\Exception\\BatchTransferExceptionTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Common/Exception/BatchTransferExceptionTest.php', + 'Guzzle\\Tests\\Common\\Exception\\ExceptionCollectionTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Common/Exception/ExceptionCollectionTest.php', + 'Guzzle\\Tests\\Common\\VersionTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Common/VersionTest.php', + 'Guzzle\\Tests\\GuzzleTestCase' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/GuzzleTestCase.php', + 'Guzzle\\Tests\\Http\\AbstractEntityBodyDecoratorTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Http/AbstractEntityBodyDecoratorTest.php', + 'Guzzle\\Tests\\Http\\CachingEntityBodyTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Http/CachingEntityBodyTest.php', + 'Guzzle\\Tests\\Http\\ClientTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Http/ClientTest.php', + 'Guzzle\\Tests\\Http\\CommaAggregatorTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Http/QueryAggregator/CommaAggregatorTest.php', + 'Guzzle\\Tests\\Http\\Curl\\CurlHandleTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Http/Curl/CurlHandleTest.php', + 'Guzzle\\Tests\\Http\\Curl\\CurlMultiProxyTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Http/Curl/CurlMultiProxyTest.php', + 'Guzzle\\Tests\\Http\\Curl\\CurlMultiTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Http/Curl/CurlMultiTest.php', + 'Guzzle\\Tests\\Http\\Curl\\CurlVersionTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Http/Curl/CurlVersionTest.php', + 'Guzzle\\Tests\\Http\\Curl\\RequestMediatorTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Http/Curl/RequestMediatorTest.php', + 'Guzzle\\Tests\\Http\\DuplicateAggregatorTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Http/QueryAggregator/DuplicateAggregatorTest.php', + 'Guzzle\\Tests\\Http\\EntityBodyTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Http/EntityBodyTest.php', + 'Guzzle\\Tests\\Http\\Exception\\CurlExceptionTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Http/Exception/CurlExceptionTest.php', + 'Guzzle\\Tests\\Http\\Exception\\ExceptionTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Http/Exception/ExceptionTest.php', + 'Guzzle\\Tests\\Http\\Exception\\MultiTransferExceptionTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Http/Exception/MultiTransferExceptionTest.php', + 'Guzzle\\Tests\\Http\\IoEmittingEntityBodyTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Http/IoEmittingEntityBodyTest.php', + 'Guzzle\\Tests\\Http\\Message\\AbstractMessageTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Http/Message/AbstractMessageTest.php', + 'Guzzle\\Tests\\Http\\Message\\EntityEnclosingRequestTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Http/Message/EntityEnclosingRequestTest.php', + 'Guzzle\\Tests\\Http\\Message\\HeaderComparison' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Http/Message/HeaderComparison.php', + 'Guzzle\\Tests\\Http\\Message\\HeaderTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Http/Message/HeaderTest.php', + 'Guzzle\\Tests\\Http\\Message\\Header\\HeaderFactoryTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Http/Message/Header/HeaderFactoryTest.php', + 'Guzzle\\Tests\\Http\\Message\\Header\\LinkTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Http/Message/Header/LinkTest.php', + 'Guzzle\\Tests\\Http\\Message\\HttpRequestFactoryTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Http/Message/RequestFactoryTest.php', + 'Guzzle\\Tests\\Http\\Message\\PostFileTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Http/Message/PostFileTest.php', + 'Guzzle\\Tests\\Http\\Message\\RequestTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Http/Message/RequestTest.php', + 'Guzzle\\Tests\\Http\\MimetypesTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Http/MimetypesTest.php', + 'Guzzle\\Tests\\Http\\PhpAggregatorTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Http/QueryAggregator/PhpAggregatorTest.php', + 'Guzzle\\Tests\\Http\\QueryStringTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Http/QueryStringTest.php', + 'Guzzle\\Tests\\Http\\ReadLimitEntityBodyTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Http/ReadLimitEntityBodyTest.php', + 'Guzzle\\Tests\\Http\\Server' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Http/Server.php', + 'Guzzle\\Tests\\Http\\UrlTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Http/UrlTest.php', + 'Guzzle\\Tests\\Inflection\\InflectorTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Inflection/InflectorTest.php', + 'Guzzle\\Tests\\Inflection\\MemoizingInflectorTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Inflection/MemoizingInflectorTest.php', + 'Guzzle\\Tests\\Inflection\\PreComputedInflectorTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Inflection/PreComputedInflectorTest.php', + 'Guzzle\\Tests\\Iterator\\AppendIteratorTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Iterator/AppendIteratorTest.php', + 'Guzzle\\Tests\\Iterator\\ChunkedIteratorTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Iterator/ChunkedIteratorTest.php', + 'Guzzle\\Tests\\Iterator\\FilterIteratorTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Iterator/FilterIteratorTest.php', + 'Guzzle\\Tests\\Iterator\\MapIteratorTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Iterator/MapIteratorTest.php', + 'Guzzle\\Tests\\Iterator\\MethodProxyIteratorTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Iterator/MethodProxyIteratorTest.php', + 'Guzzle\\Tests\\Log\\ArrayLogAdapterTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Log/ArrayLogAdapterTest.php', + 'Guzzle\\Tests\\Log\\ClosureLogAdapterTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Log/ClosureLogAdapterTest.php', + 'Guzzle\\Tests\\Log\\MessageFormatterTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Log/MessageFormatterTest.php', + 'Guzzle\\Tests\\Log\\PsrLogAdapterTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Log/PsrLogAdapterTest.php', + 'Guzzle\\Tests\\Log\\Zf2LogAdapterTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Log/Zf2LogAdapterTest.php', + 'Guzzle\\Tests\\Message\\HeaderComparisonTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Http/Message/HeaderComparisonTest.php', + 'Guzzle\\Tests\\Message\\ResponseTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Http/Message/ResponseTest.php', + 'Guzzle\\Tests\\Mock\\CustomResponseModel' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Mock/CustomResponseModel.php', + 'Guzzle\\Tests\\Mock\\ErrorResponseMock' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Mock/ErrorResponseMock.php', + 'Guzzle\\Tests\\Mock\\ExceptionMock' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Mock/ExceptionMock.php', + 'Guzzle\\Tests\\Mock\\MockMulti' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Mock/MockMulti.php', + 'Guzzle\\Tests\\Mock\\MockObserver' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Mock/MockObserver.php', + 'Guzzle\\Tests\\Mock\\MockSubject' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Mock/MockSubject.php', + 'Guzzle\\Tests\\Parser\\Cookie\\CookieParserProvider' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Parser/Cookie/CookieParserProvider.php', + 'Guzzle\\Tests\\Parser\\Cookie\\CookieParserTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Parser/Cookie/CookieParserTest.php', + 'Guzzle\\Tests\\Parser\\Message\\MessageParserProvider' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Parser/Message/MessageParserProvider.php', + 'Guzzle\\Tests\\Parser\\Message\\MessageParserTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Parser/Message/MessageParserTest.php', + 'Guzzle\\Tests\\Parser\\Message\\PeclHttpMessageParserTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Parser/Message/PeclHttpMessageParserTest.php', + 'Guzzle\\Tests\\Parser\\ParserRegistryTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Parser/ParserRegistryTest.php', + 'Guzzle\\Tests\\Parsers\\UriTemplate\\AbstractUriTemplateTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Parser/UriTemplate/AbstractUriTemplateTest.php', + 'Guzzle\\Tests\\Parsers\\UriTemplate\\PeclUriTemplateTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Parser/UriTemplate/PeclUriTemplateTest.php', + 'Guzzle\\Tests\\Parsers\\UriTemplate\\UriTemplateTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Parser/UriTemplate/UriTemplateTest.php', + 'Guzzle\\Tests\\Plugin\\Async\\AsyncPluginTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Async/AsyncPluginTest.php', + 'Guzzle\\Tests\\Plugin\\Backoff\\AbstractBackoffStrategyTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/AbstractBackoffStrategyTest.php', + 'Guzzle\\Tests\\Plugin\\Backoff\\BackoffLoggerTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/BackoffLoggerTest.php', + 'Guzzle\\Tests\\Plugin\\Backoff\\BackoffPluginTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/BackoffPluginTest.php', + 'Guzzle\\Tests\\Plugin\\Backoff\\CallbackBackoffStrategyTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/CallbackBackoffStrategyTest.php', + 'Guzzle\\Tests\\Plugin\\Backoff\\ConstantBackoffStrategyTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/ConstantBackoffStrategyTest.php', + 'Guzzle\\Tests\\Plugin\\Backoff\\CurlBackoffStrategyTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/CurlBackoffStrategyTest.php', + 'Guzzle\\Tests\\Plugin\\Backoff\\ExponentialBackoffStrategyTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/ExponentialBackoffStrategyTest.php', + 'Guzzle\\Tests\\Plugin\\Backoff\\HttpBackoffStrategyTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/HttpBackoffStrategyTest.php', + 'Guzzle\\Tests\\Plugin\\Backoff\\LinearBackoffStrategyTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/LinearBackoffStrategyTest.php', + 'Guzzle\\Tests\\Plugin\\Backoff\\ReasonPhraseBackoffStrategyTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/ReasonPhraseBackoffStrategyTest.php', + 'Guzzle\\Tests\\Plugin\\Backoff\\TruncatedBackoffStrategyTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/TruncatedBackoffStrategyTest.php', + 'Guzzle\\Tests\\Plugin\\Cache\\CachePluginTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cache/CachePluginTest.php', + 'Guzzle\\Tests\\Plugin\\Cache\\CallbackCanCacheStrategyTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cache/CallbackCanCacheStrategyTest.php', + 'Guzzle\\Tests\\Plugin\\Cache\\DefaultCacheStorageTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cache/DefaultCacheStorageTest.php', + 'Guzzle\\Tests\\Plugin\\Cache\\DefaultCanCacheStrategyTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cache/DefaultCanCacheStrategyTest.php', + 'Guzzle\\Tests\\Plugin\\Cache\\DefaultRevalidationTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cache/DefaultRevalidationTest.php', + 'Guzzle\\Tests\\Plugin\\Cache\\DenyRevalidationTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cache/DenyRevalidationTest.php', + 'Guzzle\\Tests\\Plugin\\Cache\\SkipRevalidationTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cache/SkipRevalidationTest.php', + 'Guzzle\\Tests\\Plugin\\Cookie\\CookieJar\\ArrayCookieJarTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cookie/CookieJar/ArrayCookieJarTest.php', + 'Guzzle\\Tests\\Plugin\\Cookie\\CookieJar\\FileCookieJarTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cookie/CookieJar/FileCookieJarTest.php', + 'Guzzle\\Tests\\Plugin\\Cookie\\CookiePluginTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cookie/CookiePluginTest.php', + 'Guzzle\\Tests\\Plugin\\Cookie\\CookieTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cookie/CookieTest.php', + 'Guzzle\\Tests\\Plugin\\CurlAuth\\CurlAuthPluginTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/CurlAuth/CurlAuthPluginTest.php', + 'Guzzle\\Tests\\Plugin\\ErrorResponse\\ErrorResponsePluginTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/ErrorResponse/ErrorResponsePluginTest.php', + 'Guzzle\\Tests\\Plugin\\History\\HistoryPluginTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/History/HistoryPluginTest.php', + 'Guzzle\\Tests\\Plugin\\Log\\LogPluginTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Log/LogPluginTest.php', + 'Guzzle\\Tests\\Plugin\\Md5\\CommandContentMd5PluginTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Md5/CommandContentMd5PluginTest.php', + 'Guzzle\\Tests\\Plugin\\Md5\\Md5ValidatorPluginTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Md5/Md5ValidatorPluginTest.php', + 'Guzzle\\Tests\\Plugin\\Mock\\MockPluginTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Mock/MockPluginTest.php', + 'Guzzle\\Tests\\Plugin\\Oauth\\OauthPluginTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Oauth/OauthPluginTest.php', + 'Guzzle\\Tests\\Plugin\\Redirect\\RedirectPluginTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Http/RedirectPluginTest.php', + 'Guzzle\\Tests\\Plugin\\Redirect\\StaticClientTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Http/StaticClientTest.php', + 'Guzzle\\Tests\\Service\\AbstractConfigLoaderTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Service/AbstractConfigLoaderTest.php', + 'Guzzle\\Tests\\Service\\Builder\\ServiceBuilderLoaderTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Builder/ServiceBuilderLoaderTest.php', + 'Guzzle\\Tests\\Service\\CachingConfigLoaderTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Service/CachingConfigLoaderTest.php', + 'Guzzle\\Tests\\Service\\ClientTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Service/ClientTest.php', + 'Guzzle\\Tests\\Service\\Command\\AbstractCommandTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/AbstractCommandTest.php', + 'Guzzle\\Tests\\Service\\Command\\AliasFactoryTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/Factory/AliasFactoryTest.php', + 'Guzzle\\Tests\\Service\\Command\\ClosureCommandTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/ClosureCommandTest.php', + 'Guzzle\\Tests\\Service\\Command\\CommandTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/CommandTest.php', + 'Guzzle\\Tests\\Service\\Command\\CompositeFactoryTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/Factory/CompositeFactoryTest.php', + 'Guzzle\\Tests\\Service\\Command\\ConcreteClassFactoryTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/Factory/ConcreteClassFactoryTest.php', + 'Guzzle\\Tests\\Service\\Command\\DefaultRequestSerializerTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/DefaultRequestSerializerTest.php', + 'Guzzle\\Tests\\Service\\Command\\DefaultResponseParserTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/DefaultResponseParserTest.php', + 'Guzzle\\Tests\\Service\\Command\\LocationVisitor\\Request\\AbstractVisitorTestCase' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Request/AbstractVisitorTestCase.php', + 'Guzzle\\Tests\\Service\\Command\\LocationVisitor\\Request\\BodyVisitorTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Request/BodyVisitorTest.php', + 'Guzzle\\Tests\\Service\\Command\\LocationVisitor\\Request\\HeaderVisitorTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Request/HeaderVisitorTest.php', + 'Guzzle\\Tests\\Service\\Command\\LocationVisitor\\Request\\JsonVisitorTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Request/JsonVisitorTest.php', + 'Guzzle\\Tests\\Service\\Command\\LocationVisitor\\Request\\PostFieldVisitorTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Request/PostFieldVisitorTest.php', + 'Guzzle\\Tests\\Service\\Command\\LocationVisitor\\Request\\PostFileVisitorTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Request/PostFileVisitorTest.php', + 'Guzzle\\Tests\\Service\\Command\\LocationVisitor\\Request\\QueryVisitorTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Request/QueryVisitorTest.php', + 'Guzzle\\Tests\\Service\\Command\\LocationVisitor\\Request\\ResponseBodyVisitorTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Request/ResponseBodyVisitorTest.php', + 'Guzzle\\Tests\\Service\\Command\\LocationVisitor\\Request\\XmlVisitorTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Request/XmlVisitorTest.php', + 'Guzzle\\Tests\\Service\\Command\\LocationVisitor\\Response\\AbstractResponseVisitorTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Response/AbstractResponseVisitorTest.php', + 'Guzzle\\Tests\\Service\\Command\\LocationVisitor\\Response\\BodyVisitorTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Response/BodyVisitorTest.php', + 'Guzzle\\Tests\\Service\\Command\\LocationVisitor\\Response\\HeaderVisitorTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Response/HeaderVisitorTest.php', + 'Guzzle\\Tests\\Service\\Command\\LocationVisitor\\Response\\JsonVisitorTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Response/JsonVisitorTest.php', + 'Guzzle\\Tests\\Service\\Command\\LocationVisitor\\Response\\ReasonPhraseVisitorTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Response/ReasonPhraseVisitorTest.php', + 'Guzzle\\Tests\\Service\\Command\\LocationVisitor\\Response\\StatusCodeVisitorTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Response/StatusCodeVisitorTest.php', + 'Guzzle\\Tests\\Service\\Command\\LocationVisitor\\Response\\XmlVisitorTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Response/XmlVisitorTest.php', + 'Guzzle\\Tests\\Service\\Command\\MapFactoryTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/Factory/MapFactoryTest.php', + 'Guzzle\\Tests\\Service\\Command\\OperationCommandTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/OperationCommandTest.php', + 'Guzzle\\Tests\\Service\\Command\\OperationResponseParserTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/OperationResponseParserTest.php', + 'Guzzle\\Tests\\Service\\Command\\ServiceDescriptionFactoryTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/Factory/ServiceDescriptionFactoryTest.php', + 'Guzzle\\Tests\\Service\\Command\\VisitorFlyweightTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/VisitorFlyweightTest.php', + 'Guzzle\\Tests\\Service\\Description\\OperationTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Description/OperationTest.php', + 'Guzzle\\Tests\\Service\\Description\\ParameterTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Description/ParameterTest.php', + 'Guzzle\\Tests\\Service\\Description\\SchemaFormatterTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Description/SchemaFormatterTest.php', + 'Guzzle\\Tests\\Service\\Description\\SchemaValidatorTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Description/SchemaValidatorTest.php', + 'Guzzle\\Tests\\Service\\Description\\ServiceDescriptionLoaderTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Description/ServiceDescriptionLoaderTest.php', + 'Guzzle\\Tests\\Service\\Description\\ServiceDescriptionTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Description/ServiceDescriptionTest.php', + 'Guzzle\\Tests\\Service\\Exception\\CommandTransferExceptionTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Exception/CommandTransferExceptionTest.php', + 'Guzzle\\Tests\\Service\\Exception\\InconsistentClientTransferExceptionTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Exception/InconsistentClientTransferExceptionTest.php', + 'Guzzle\\Tests\\Service\\Exception\\ValidationExceptionTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Exception/ValidationExceptionTest.php', + 'Guzzle\\Tests\\Service\\Mock\\Command\\IterableCommand' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Mock/Command/IterableCommand.php', + 'Guzzle\\Tests\\Service\\Mock\\Command\\MockCommand' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Mock/Command/MockCommand.php', + 'Guzzle\\Tests\\Service\\Mock\\Command\\OtherCommand' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Mock/Command/OtherCommand.php', + 'Guzzle\\Tests\\Service\\Mock\\Command\\Sub\\Sub' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Mock/Command/Sub/Sub.php', + 'Guzzle\\Tests\\Service\\Mock\\MockClient' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Mock/MockClient.php', + 'Guzzle\\Tests\\Service\\Mock\\Model\\MockCommandIterator' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Mock/Model/MockCommandIterator.php', + 'Guzzle\\Tests\\Service\\Resource\\CompositeResourceIteratorFactoryTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Resource/CompositeResourceIteratorFactoryTest.php', + 'Guzzle\\Tests\\Service\\Resource\\MapResourceIteratorFactoryTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Resource/MapResourceIteratorFactoryTest.php', + 'Guzzle\\Tests\\Service\\Resource\\ModelTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Resource/ModelTest.php', + 'Guzzle\\Tests\\Service\\Resource\\ResourceIteratorClassFactoryTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Resource/ResourceIteratorClassFactoryTest.php', + 'Guzzle\\Tests\\Service\\Resource\\ResourceIteratorTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Resource/ResourceIteratorTest.php', + 'Guzzle\\Tests\\Service\\ServiceBuilderTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Builder/ServiceBuilderTest.php', + 'Guzzle\\Tests\\Stream\\PhpStreamRequestFactoryTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Stream/PhpStreamRequestFactoryTest.php', + 'Guzzle\\Tests\\Stream\\StreamTest' => $vendorDir . '/guzzle/guzzle/tests/Guzzle/Tests/Stream/StreamTest.php', 'ID3Parser\\ID3Parser' => $vendorDir . '/lukasreschke/id3parser/src/ID3Parser.php', 'ID3Parser\\getID3\\Tags\\getid3_id3v1' => $vendorDir . '/lukasreschke/id3parser/src/getID3/Tags/getid3_id3v1.php', 'ID3Parser\\getID3\\Tags\\getid3_id3v2' => $vendorDir . '/lukasreschke/id3parser/src/getID3/Tags/getid3_id3v2.php', @@ -615,6 +942,20 @@ 'Icewind\\Streams\\PathWrapper' => $vendorDir . '/icewind/streams/src/PathWrapper.php', 'Icewind\\Streams\\RetryWrapper' => $vendorDir . '/icewind/streams/src/RetryWrapper.php', 'Icewind\\Streams\\SeekableWrapper' => $vendorDir . '/icewind/streams/src/SeekableWrapper.php', + 'Icewind\\Streams\\Tests\\CallbackWrapper' => $vendorDir . '/icewind/streams/tests/CallbackWrapper.php', + 'Icewind\\Streams\\Tests\\DirectoryFilter' => $vendorDir . '/icewind/streams/tests/DirectoryFilter.php', + 'Icewind\\Streams\\Tests\\DirectoryWrapper' => $vendorDir . '/icewind/streams/tests/DirectoryWrapper.php', + 'Icewind\\Streams\\Tests\\DirectoryWrapperDummy' => $vendorDir . '/icewind/streams/tests/DirectoryWrapper.php', + 'Icewind\\Streams\\Tests\\DirectoryWrapperNull' => $vendorDir . '/icewind/streams/tests/DirectoryWrapper.php', + 'Icewind\\Streams\\Tests\\FailWrapper' => $vendorDir . '/icewind/streams/tests/RetryWrapper.php', + 'Icewind\\Streams\\Tests\\IteratorDirectory' => $vendorDir . '/icewind/streams/tests/IteratorDirectory.php', + 'Icewind\\Streams\\Tests\\NullWrapper' => $vendorDir . '/icewind/streams/tests/NullWrapper.php', + 'Icewind\\Streams\\Tests\\PartialWrapper' => $vendorDir . '/icewind/streams/tests/RetryWrapper.php', + 'Icewind\\Streams\\Tests\\PathWrapper' => $vendorDir . '/icewind/streams/tests/PathWrapper.php', + 'Icewind\\Streams\\Tests\\RetryWrapper' => $vendorDir . '/icewind/streams/tests/RetryWrapper.php', + 'Icewind\\Streams\\Tests\\SeekableWrapper' => $vendorDir . '/icewind/streams/tests/SeekableWrapper.php', + 'Icewind\\Streams\\Tests\\UrlCallBack' => $vendorDir . '/icewind/streams/tests/UrlCallBack.php', + 'Icewind\\Streams\\Tests\\Wrapper' => $vendorDir . '/icewind/streams/tests/Wrapper.php', 'Icewind\\Streams\\Url' => $vendorDir . '/icewind/streams/src/Url.php', 'Icewind\\Streams\\UrlCallback' => $vendorDir . '/icewind/streams/src/UrlCallBack.php', 'Icewind\\Streams\\Wrapper' => $vendorDir . '/icewind/streams/src/Wrapper.php', @@ -697,6 +1038,10 @@ 'OpenCloud\\Autoscale\\Resource\\ScalingPolicy' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Autoscale/Resource/ScalingPolicy.php', 'OpenCloud\\Autoscale\\Resource\\Webhook' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Autoscale/Resource/Webhook.php', 'OpenCloud\\Autoscale\\Service' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Autoscale/Service.php', + 'OpenCloud\\CDN\\Resource\\Flavor' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/CDN/Resource/Flavor.php', + 'OpenCloud\\CDN\\Resource\\Service' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/CDN/Resource/Service.php', + 'OpenCloud\\CDN\\Service' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/CDN/Service.php', + 'OpenCloud\\CloudMonitoring\\Collection\\MonitoringIterator' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Collection/MonitoringIterator.php', 'OpenCloud\\CloudMonitoring\\Exception\\AgentException' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Exception/AgentException.php', 'OpenCloud\\CloudMonitoring\\Exception\\AlarmException' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Exception/AlarmException.php', 'OpenCloud\\CloudMonitoring\\Exception\\CheckException' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Exception/CheckException.php', @@ -721,14 +1066,16 @@ 'OpenCloud\\CloudMonitoring\\Resource\\CheckType' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/CheckType.php', 'OpenCloud\\CloudMonitoring\\Resource\\Entity' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/Entity.php', 'OpenCloud\\CloudMonitoring\\Resource\\Metric' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/Metric.php', + 'OpenCloud\\CloudMonitoring\\Resource\\MetricDataPoint' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/MetricDataPoint.php', 'OpenCloud\\CloudMonitoring\\Resource\\Notification' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/Notification.php', 'OpenCloud\\CloudMonitoring\\Resource\\NotificationHistory' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/NotificationHistory.php', 'OpenCloud\\CloudMonitoring\\Resource\\NotificationPlan' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/NotificationPlan.php', 'OpenCloud\\CloudMonitoring\\Resource\\NotificationType' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/NotificationType.php', - 'OpenCloud\\CloudMonitoring\\Resource\\ReadonlyResource' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/ReadOnlyResource.php', + 'OpenCloud\\CloudMonitoring\\Resource\\ReadOnlyResource' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/ReadOnlyResource.php', 'OpenCloud\\CloudMonitoring\\Resource\\View' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/View.php', 'OpenCloud\\CloudMonitoring\\Resource\\Zone' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/Zone.php', 'OpenCloud\\CloudMonitoring\\Service' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Service.php', + 'OpenCloud\\Common\\ArrayAccess' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Common/ArrayAccess.php', 'OpenCloud\\Common\\Base' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Common/Base.php', 'OpenCloud\\Common\\Collection' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Common/Collection.php', 'OpenCloud\\Common\\Collection\\ArrayCollection' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Common/Collection/ArrayCollection.php', @@ -745,6 +1092,8 @@ 'OpenCloud\\Common\\Exceptions\\AsyncTimeoutError' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/AsyncTimeoutError.php', 'OpenCloud\\Common\\Exceptions\\AttributeError' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/AttributeError.php', 'OpenCloud\\Common\\Exceptions\\AuthenticationError' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/AuthenticationError.php', + 'OpenCloud\\Common\\Exceptions\\BackupInstanceError' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/BackupInstanceError.php', + 'OpenCloud\\Common\\Exceptions\\BackupNameError' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/BackupNameError.php', 'OpenCloud\\Common\\Exceptions\\BaseException' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/BaseException.php', 'OpenCloud\\Common\\Exceptions\\CdnError' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/CdnError.php', 'OpenCloud\\Common\\Exceptions\\CdnHttpError' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/CdnHttpError.php', @@ -768,12 +1117,15 @@ 'OpenCloud\\Common\\Exceptions\\DeleteError' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/DeleteError.php', 'OpenCloud\\Common\\Exceptions\\DocumentError' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/DocumentError.php', 'OpenCloud\\Common\\Exceptions\\DomainError' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/DomainError.php', + 'OpenCloud\\Common\\Exceptions\\DomainNotFoundException' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/DomainNotFoundException.php', 'OpenCloud\\Common\\Exceptions\\EmptyResponseError' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/EmptyResponseError.php', 'OpenCloud\\Common\\Exceptions\\EndpointError' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/EndpointError.php', 'OpenCloud\\Common\\Exceptions\\FlavorError' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/FlavorError.php', + 'OpenCloud\\Common\\Exceptions\\ForbiddenOperationException' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/ForbiddenOperationException.php', 'OpenCloud\\Common\\Exceptions\\HttpError' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/HttpError.php', 'OpenCloud\\Common\\Exceptions\\HttpForbiddenError' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/HttpForbiddenError.php', 'OpenCloud\\Common\\Exceptions\\HttpOverLimitError' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/HttpOverLimitError.php', + 'OpenCloud\\Common\\Exceptions\\HttpResponseException' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/HttpResponseException.php', 'OpenCloud\\Common\\Exceptions\\HttpRetryError' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/HttpRetryError.php', 'OpenCloud\\Common\\Exceptions\\HttpTimeoutError' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/HttpTimeoutError.php', 'OpenCloud\\Common\\Exceptions\\HttpUnauthorizedError' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/HttpUnauthorizedError.php', @@ -792,6 +1144,7 @@ 'OpenCloud\\Common\\Exceptions\\InvalidIpTypeError' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/InvalidIpTypeError.php', 'OpenCloud\\Common\\Exceptions\\InvalidParameterError' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/InvalidParameterError.php', 'OpenCloud\\Common\\Exceptions\\InvalidRequestError' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/InvalidRequestError.php', + 'OpenCloud\\Common\\Exceptions\\InvalidTemplateError' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/InvalidTemplateError.php', 'OpenCloud\\Common\\Exceptions\\JsonError' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/JsonError.php', 'OpenCloud\\Common\\Exceptions\\LoggingException' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/LoggingException.php', 'OpenCloud\\Common\\Exceptions\\MetadataCreateError' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/MetadataCreateError.php', @@ -817,6 +1170,7 @@ 'OpenCloud\\Common\\Exceptions\\RebuildError' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/RebuildError.php', 'OpenCloud\\Common\\Exceptions\\RecordTypeError' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/RecordTypeError.php', 'OpenCloud\\Common\\Exceptions\\ResourceBucketException' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/ResourceBucketException.php', + 'OpenCloud\\Common\\Exceptions\\ResourceNotFoundException' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/ResourceNotFoundException.php', 'OpenCloud\\Common\\Exceptions\\RuntimeException' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/RuntimeException.php', 'OpenCloud\\Common\\Exceptions\\ServerActionError' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/ServerActionError.php', 'OpenCloud\\Common\\Exceptions\\ServerCreateError' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/ServerCreateError.php', @@ -848,12 +1202,13 @@ 'OpenCloud\\Common\\Http\\Message\\Formatter' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Common/Http/Message/Formatter.php', 'OpenCloud\\Common\\Http\\Message\\RequestSubscriber' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Common/Http/Message/RequestSubscriber.php', 'OpenCloud\\Common\\Lang' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Common/Lang.php', - 'OpenCloud\\Common\\Log\\AbstractLogger' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Common/Log/AbstractLogger.php', - 'OpenCloud\\Common\\Log\\LogLevel' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Common/Log/LogLevel.php', 'OpenCloud\\Common\\Log\\Logger' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Common/Log/Logger.php', - 'OpenCloud\\Common\\Log\\LoggerInterface' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Common/Log/LoggerInterface.php', 'OpenCloud\\Common\\Metadata' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Common/Metadata.php', 'OpenCloud\\Common\\PersistentObject' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Common/PersistentObject.php', + 'OpenCloud\\Common\\Resource\\BaseResource' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Common/Resource/BaseResource.php', + 'OpenCloud\\Common\\Resource\\NovaResource' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Common/Resource/NovaResource.php', + 'OpenCloud\\Common\\Resource\\PersistentResource' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Common/Resource/PersistentResource.php', + 'OpenCloud\\Common\\Resource\\ReadOnlyResource' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Common/Resource/ReadOnlyResource.php', 'OpenCloud\\Common\\Service\\AbstractService' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Common/Service/AbstractService.php', 'OpenCloud\\Common\\Service\\Catalog' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Common/Service/Catalog.php', 'OpenCloud\\Common\\Service\\CatalogItem' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Common/Service/CatalogItem.php', @@ -875,14 +1230,19 @@ 'OpenCloud\\Compute\\Resource\\VolumeAttachment' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Compute/Resource/VolumeAttachment.php', 'OpenCloud\\Compute\\Service' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Compute/Service.php', 'OpenCloud\\DNS\\Collection\\DnsIterator' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/DNS/Collection/DnsIterator.php', + 'OpenCloud\\DNS\\Resource\\AbstractResource' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/DNS/Resource/AbstractResource.php', 'OpenCloud\\DNS\\Resource\\AsyncResponse' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/DNS/Resource/AsyncResponse.php', 'OpenCloud\\DNS\\Resource\\Domain' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/DNS/Resource/Domain.php', - 'OpenCloud\\DNS\\Resource\\Object' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/DNS/Resource/Object.php', + 'OpenCloud\\DNS\\Resource\\HasPtrRecordsInterface' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/DNS/Resource/HasPtrRecordsInterface.php', 'OpenCloud\\DNS\\Resource\\PtrRecord' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/DNS/Resource/PtrRecord.php', 'OpenCloud\\DNS\\Resource\\Record' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/DNS/Resource/Record.php', 'OpenCloud\\DNS\\Resource\\Subdomain' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/DNS/Resource/Subdomain.php', 'OpenCloud\\DNS\\Service' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/DNS/Service.php', + 'OpenCloud\\Database\\Resource\\Backup' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Database/Resource/Backup.php', + 'OpenCloud\\Database\\Resource\\Configuration' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Database/Resource/Configuration.php', 'OpenCloud\\Database\\Resource\\Database' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Database/Resource/Database.php', + 'OpenCloud\\Database\\Resource\\Datastore' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Database/Resource/Datastore.php', + 'OpenCloud\\Database\\Resource\\DatastoreVersion' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Database/Resource/DatastoreVersion.php', 'OpenCloud\\Database\\Resource\\Instance' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Database/Resource/Instance.php', 'OpenCloud\\Database\\Resource\\User' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Database/Resource/User.php', 'OpenCloud\\Database\\Service' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Database/Service.php', @@ -892,10 +1252,30 @@ 'OpenCloud\\Identity\\Resource\\Token' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Identity/Resource/Token.php', 'OpenCloud\\Identity\\Resource\\User' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Identity/Resource/User.php', 'OpenCloud\\Identity\\Service' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Identity/Service.php', + 'OpenCloud\\Image\\Enum\\Document' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Image/Enum/Document.php', + 'OpenCloud\\Image\\Enum\\MemberStatus' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Image/Enum/MemberStatus.php', + 'OpenCloud\\Image\\Enum\\OperationType' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Image/Enum/OperationType.php', + 'OpenCloud\\Image\\Enum\\Schema' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Image/Enum/Schema.php', + 'OpenCloud\\Image\\Resource\\AbstractSchemaResource' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Image/Resource/AbstractSchemaResource.php', + 'OpenCloud\\Image\\Resource\\Image' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Image/Resource/Image.php', + 'OpenCloud\\Image\\Resource\\ImageInterface' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Image/Resource/ImageInterface.php', + 'OpenCloud\\Image\\Resource\\JsonPatch\\Document' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Image/Resource/JsonPatch/Document.php', + 'OpenCloud\\Image\\Resource\\JsonPatch\\Encoder' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Image/Resource/JsonPatch/Encoder.php', + 'OpenCloud\\Image\\Resource\\JsonPatch\\Operation' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Image/Resource/JsonPatch/Operation.php', + 'OpenCloud\\Image\\Resource\\Member' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Image/Resource/Member.php', + 'OpenCloud\\Image\\Resource\\Schema\\AbstractSchemaItem' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Image/Resource/Schema/AbstractSchemaItem.php', + 'OpenCloud\\Image\\Resource\\Schema\\Property' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Image/Resource/Schema/Property.php', + 'OpenCloud\\Image\\Resource\\Schema\\Schema' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Image/Resource/Schema/Schema.php', + 'OpenCloud\\Image\\Service' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Image/Service.php', + 'OpenCloud\\LoadBalancer\\Collection\\LoadBalancerIterator' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Collection/LoadBalancerIterator.php', + 'OpenCloud\\LoadBalancer\\Enum\\IpType' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Enum/IpType.php', + 'OpenCloud\\LoadBalancer\\Enum\\NodeCondition' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Enum/NodeCondition.php', + 'OpenCloud\\LoadBalancer\\Enum\\NodeType' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Enum/NodeType.php', + 'OpenCloud\\LoadBalancer\\Resource\\AbstractResource' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/AbstractResource.php', 'OpenCloud\\LoadBalancer\\Resource\\Access' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/Access.php', 'OpenCloud\\LoadBalancer\\Resource\\Algorithm' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/Algorithm.php', 'OpenCloud\\LoadBalancer\\Resource\\AllowedDomain' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/AllowedDomain.php', - 'OpenCloud\\LoadBalancer\\Resource\\BillableLoadBalancer' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/BillableLoadBalancer.php', + 'OpenCloud\\LoadBalancer\\Resource\\CertificateMapping' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/CertificateMapping.php', 'OpenCloud\\LoadBalancer\\Resource\\ConnectionLogging' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/ConnectionLogging.php', 'OpenCloud\\LoadBalancer\\Resource\\ConnectionThrottle' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/ConnectionThrottle.php', 'OpenCloud\\LoadBalancer\\Resource\\ContentCaching' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/ContentCaching.php', @@ -905,21 +1285,30 @@ 'OpenCloud\\LoadBalancer\\Resource\\Metadata' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/Metadata.php', 'OpenCloud\\LoadBalancer\\Resource\\Node' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/Node.php', 'OpenCloud\\LoadBalancer\\Resource\\NodeEvent' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/NodeEvent.php', + 'OpenCloud\\LoadBalancer\\Resource\\NonIdUriResource' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/NonIdUriResource.php', 'OpenCloud\\LoadBalancer\\Resource\\Protocol' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/Protocol.php', - 'OpenCloud\\LoadBalancer\\Resource\\Readonly' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/Readonly.php', + 'OpenCloud\\LoadBalancer\\Resource\\ReadOnlyResource' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/ReadOnlyResource.php', 'OpenCloud\\LoadBalancer\\Resource\\SSLTermination' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/SSLTermination.php', 'OpenCloud\\LoadBalancer\\Resource\\SessionPersistence' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/SessionPersistence.php', 'OpenCloud\\LoadBalancer\\Resource\\Stats' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/Stats.php', - 'OpenCloud\\LoadBalancer\\Resource\\SubResource' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/SubResource.php', - 'OpenCloud\\LoadBalancer\\Resource\\Usage' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/Usage.php', + 'OpenCloud\\LoadBalancer\\Resource\\UsageRecord' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/UsageRecord.php', 'OpenCloud\\LoadBalancer\\Resource\\VirtualIp' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/VirtualIp.php', 'OpenCloud\\LoadBalancer\\Service' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Service.php', + 'OpenCloud\\Networking\\Resource\\Network' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Networking/Resource/Network.php', + 'OpenCloud\\Networking\\Resource\\NetworkInterface' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Networking/Resource/NetworkInterface.php', + 'OpenCloud\\Networking\\Resource\\Port' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Networking/Resource/Port.php', + 'OpenCloud\\Networking\\Resource\\SecurityGroup' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Networking/Resource/SecurityGroup.php', + 'OpenCloud\\Networking\\Resource\\SecurityGroupRule' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Networking/Resource/SecurityGroupRule.php', + 'OpenCloud\\Networking\\Resource\\Subnet' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Networking/Resource/Subnet.php', + 'OpenCloud\\Networking\\Service' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Networking/Service.php', 'OpenCloud\\ObjectStore\\AbstractService' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/AbstractService.php', 'OpenCloud\\ObjectStore\\CDNService' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/CDNService.php', 'OpenCloud\\ObjectStore\\Constants\\Header' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Constants/Header.php', 'OpenCloud\\ObjectStore\\Constants\\UrlType' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Constants/UrlType.php', + 'OpenCloud\\ObjectStore\\Enum\\ReturnType' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Enum/ReturnType.php', 'OpenCloud\\ObjectStore\\Exception\\BulkOperationException' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Exception/BulkOperationException.php', 'OpenCloud\\ObjectStore\\Exception\\ContainerException' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Exception/ContainerException.php', + 'OpenCloud\\ObjectStore\\Exception\\ObjectNotEmptyException' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Exception/ObjectNotEmptyException.php', 'OpenCloud\\ObjectStore\\Exception\\ObjectNotFoundException' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Exception/ObjectNotFoundException.php', 'OpenCloud\\ObjectStore\\Exception\\StreamException' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Exception/StreamException.php', 'OpenCloud\\ObjectStore\\Exception\\UploadException' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Exception/UploadException.php', @@ -940,9 +1329,13 @@ 'OpenCloud\\ObjectStore\\Upload\\TransferPart' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Upload/TransferPart.php', 'OpenCloud\\ObjectStore\\Upload\\TransferState' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Upload/TransferState.php', 'OpenCloud\\OpenStack' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/OpenStack.php', - 'OpenCloud\\Orchestration\\Resource' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Orchestration/Resource.php', + 'OpenCloud\\Orchestration\\Resource\\BuildInfo' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Orchestration/Resource/BuildInfo.php', + 'OpenCloud\\Orchestration\\Resource\\Event' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Orchestration/Resource/Event.php', + 'OpenCloud\\Orchestration\\Resource\\Resource' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Orchestration/Resource/Resource.php', + 'OpenCloud\\Orchestration\\Resource\\ResourceType' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Orchestration/Resource/ResourceType.php', + 'OpenCloud\\Orchestration\\Resource\\Stack' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Orchestration/Resource/Stack.php', 'OpenCloud\\Orchestration\\Service' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Orchestration/Service.php', - 'OpenCloud\\Orchestration\\Stack' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Orchestration/Stack.php', + 'OpenCloud\\Queues\\Collection\\MessageIterator' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Queues/Collection/MessageIterator.php', 'OpenCloud\\Queues\\Exception\\DeleteMessageException' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Queues/Exception/DeleteMessageException.php', 'OpenCloud\\Queues\\Exception\\MessageException' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Queues/Exception/MessageException.php', 'OpenCloud\\Queues\\Exception\\QueueException' => $vendorDir . '/rackspace/php-opencloud/lib/OpenCloud/Queues/Exception/QueueException.php', @@ -1564,6 +1957,8 @@ 'aCssRulesetStartToken' => $vendorDir . '/natxet/CssMin/src/CssMin.php', 'aCssToken' => $vendorDir . '/natxet/CssMin/src/CssMin.php', 'bantu\\IniGetWrapper\\IniGetWrapper' => $vendorDir . '/bantu/ini-get-wrapper/src/IniGetWrapper.php', + 'mikemccabe\\JsonPatch\\JsonPatch' => $vendorDir . '/mikemccabe/json-patch-php/src/JsonPatch.php', + 'mikemccabe\\JsonPatch\\JsonPatchException' => $vendorDir . '/mikemccabe/json-patch-php/src/JsonPatch.php', 'ownCloud\\TarStreamer\\TarStreamer' => $vendorDir . '/deepdiver1975/tarstreamer/src/TarStreamer.php', 'phpseclib\\Crypt\\AES' => $vendorDir . '/phpseclib/phpseclib/phpseclib/Crypt/AES.php', 'phpseclib\\Crypt\\Base' => $vendorDir . '/phpseclib/phpseclib/phpseclib/Crypt/Base.php', diff --git a/composer/autoload_namespaces.php b/composer/autoload_namespaces.php index 7f7685a8b..850450c1a 100644 --- a/composer/autoload_namespaces.php +++ b/composer/autoload_namespaces.php @@ -8,11 +8,9 @@ return array( 'Pimple' => array($vendorDir . '/pimple/pimple/src'), 'PEAR' => array($vendorDir . '/pear/pear_exception'), - 'OpenCloud' => array($vendorDir . '/rackspace/php-opencloud/lib', $vendorDir . '/rackspace/php-opencloud/tests'), - 'Guzzle\\Stream' => array($vendorDir . '/guzzle/stream'), - 'Guzzle\\Parser' => array($vendorDir . '/guzzle/parser'), - 'Guzzle\\Http' => array($vendorDir . '/guzzle/http'), - 'Guzzle\\Common' => array($vendorDir . '/guzzle/common'), + 'OpenCloud' => array($vendorDir . '/rackspace/php-opencloud/lib'), + 'Guzzle\\Tests' => array($vendorDir . '/guzzle/guzzle/tests'), + 'Guzzle' => array($vendorDir . '/guzzle/guzzle/src'), 'Doctrine\\DBAL\\' => array($vendorDir . '/doctrine/dbal/lib'), 'Doctrine\\Common\\Lexer\\' => array($vendorDir . '/doctrine/lexer/lib'), 'Doctrine\\Common\\Inflector\\' => array($vendorDir . '/doctrine/inflector/lib'), diff --git a/composer/autoload_psr4.php b/composer/autoload_psr4.php index 30faee423..5a71a2cdf 100644 --- a/composer/autoload_psr4.php +++ b/composer/autoload_psr4.php @@ -8,6 +8,7 @@ return array( 'phpseclib\\' => array($vendorDir . '/phpseclib/phpseclib/phpseclib'), 'ownCloud\\TarStreamer\\' => array($vendorDir . '/deepdiver1975/tarstreamer/src'), + 'mikemccabe\\JsonPatch\\' => array($vendorDir . '/mikemccabe/json-patch-php/src'), 'bantu\\IniGetWrapper\\' => array($vendorDir . '/bantu/ini-get-wrapper/src'), 'ZipStreamer\\' => array($vendorDir . '/mcnetic/zipstreamer/src'), 'Symfony\\Polyfill\\Php70\\' => array($vendorDir . '/symfony/polyfill-php70'), diff --git a/composer/autoload_static.php b/composer/autoload_static.php index b740f44f1..a68ecce89 100644 --- a/composer/autoload_static.php +++ b/composer/autoload_static.php @@ -32,6 +32,10 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 array ( 'ownCloud\\TarStreamer\\' => 21, ), + 'm' => + array ( + 'mikemccabe\\JsonPatch\\' => 21, + ), 'b' => array ( 'bantu\\IniGetWrapper\\' => 20, @@ -106,6 +110,10 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 array ( 0 => __DIR__ . '/..' . '/deepdiver1975/tarstreamer/src', ), + 'mikemccabe\\JsonPatch\\' => + array ( + 0 => __DIR__ . '/..' . '/mikemccabe/json-patch-php/src', + ), 'bantu\\IniGetWrapper\\' => array ( 0 => __DIR__ . '/..' . '/bantu/ini-get-wrapper/src', @@ -270,26 +278,17 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'OpenCloud' => array ( 0 => __DIR__ . '/..' . '/rackspace/php-opencloud/lib', - 1 => __DIR__ . '/..' . '/rackspace/php-opencloud/tests', ), ), 'G' => array ( - 'Guzzle\\Stream' => - array ( - 0 => __DIR__ . '/..' . '/guzzle/stream', - ), - 'Guzzle\\Parser' => - array ( - 0 => __DIR__ . '/..' . '/guzzle/parser', - ), - 'Guzzle\\Http' => + 'Guzzle\\Tests' => array ( - 0 => __DIR__ . '/..' . '/guzzle/http', + 0 => __DIR__ . '/..' . '/guzzle/guzzle/tests', ), - 'Guzzle\\Common' => + 'Guzzle' => array ( - 0 => __DIR__ . '/..' . '/guzzle/common', + 0 => __DIR__ . '/..' . '/guzzle/guzzle/src', ), ), 'D' => @@ -848,85 +847,412 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'GuzzleHttp\\UriTemplate' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/UriTemplate.php', 'GuzzleHttp\\Url' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/Url.php', 'GuzzleHttp\\Utils' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/Utils.php', - 'Guzzle\\Common\\AbstractHasDispatcher' => __DIR__ . '/..' . '/guzzle/common/Guzzle/Common/AbstractHasDispatcher.php', - 'Guzzle\\Common\\Collection' => __DIR__ . '/..' . '/guzzle/common/Guzzle/Common/Collection.php', - 'Guzzle\\Common\\Event' => __DIR__ . '/..' . '/guzzle/common/Guzzle/Common/Event.php', - 'Guzzle\\Common\\Exception\\BadMethodCallException' => __DIR__ . '/..' . '/guzzle/common/Guzzle/Common/Exception/BadMethodCallException.php', - 'Guzzle\\Common\\Exception\\ExceptionCollection' => __DIR__ . '/..' . '/guzzle/common/Guzzle/Common/Exception/ExceptionCollection.php', - 'Guzzle\\Common\\Exception\\GuzzleException' => __DIR__ . '/..' . '/guzzle/common/Guzzle/Common/Exception/GuzzleException.php', - 'Guzzle\\Common\\Exception\\InvalidArgumentException' => __DIR__ . '/..' . '/guzzle/common/Guzzle/Common/Exception/InvalidArgumentException.php', - 'Guzzle\\Common\\Exception\\RuntimeException' => __DIR__ . '/..' . '/guzzle/common/Guzzle/Common/Exception/RuntimeException.php', - 'Guzzle\\Common\\Exception\\UnexpectedValueException' => __DIR__ . '/..' . '/guzzle/common/Guzzle/Common/Exception/UnexpectedValueException.php', - 'Guzzle\\Common\\FromConfigInterface' => __DIR__ . '/..' . '/guzzle/common/Guzzle/Common/FromConfigInterface.php', - 'Guzzle\\Common\\HasDispatcherInterface' => __DIR__ . '/..' . '/guzzle/common/Guzzle/Common/HasDispatcherInterface.php', - 'Guzzle\\Common\\ToArrayInterface' => __DIR__ . '/..' . '/guzzle/common/Guzzle/Common/ToArrayInterface.php', - 'Guzzle\\Common\\Version' => __DIR__ . '/..' . '/guzzle/common/Guzzle/Common/Version.php', - 'Guzzle\\Http\\AbstractEntityBodyDecorator' => __DIR__ . '/..' . '/guzzle/http/Guzzle/Http/AbstractEntityBodyDecorator.php', - 'Guzzle\\Http\\CachingEntityBody' => __DIR__ . '/..' . '/guzzle/http/Guzzle/Http/CachingEntityBody.php', - 'Guzzle\\Http\\Client' => __DIR__ . '/..' . '/guzzle/http/Guzzle/Http/Client.php', - 'Guzzle\\Http\\ClientInterface' => __DIR__ . '/..' . '/guzzle/http/Guzzle/Http/ClientInterface.php', - 'Guzzle\\Http\\Curl\\CurlHandle' => __DIR__ . '/..' . '/guzzle/http/Guzzle/Http/Curl/CurlHandle.php', - 'Guzzle\\Http\\Curl\\CurlMulti' => __DIR__ . '/..' . '/guzzle/http/Guzzle/Http/Curl/CurlMulti.php', - 'Guzzle\\Http\\Curl\\CurlMultiInterface' => __DIR__ . '/..' . '/guzzle/http/Guzzle/Http/Curl/CurlMultiInterface.php', - 'Guzzle\\Http\\Curl\\CurlMultiProxy' => __DIR__ . '/..' . '/guzzle/http/Guzzle/Http/Curl/CurlMultiProxy.php', - 'Guzzle\\Http\\Curl\\CurlVersion' => __DIR__ . '/..' . '/guzzle/http/Guzzle/Http/Curl/CurlVersion.php', - 'Guzzle\\Http\\Curl\\RequestMediator' => __DIR__ . '/..' . '/guzzle/http/Guzzle/Http/Curl/RequestMediator.php', - 'Guzzle\\Http\\EntityBody' => __DIR__ . '/..' . '/guzzle/http/Guzzle/Http/EntityBody.php', - 'Guzzle\\Http\\EntityBodyInterface' => __DIR__ . '/..' . '/guzzle/http/Guzzle/Http/EntityBodyInterface.php', - 'Guzzle\\Http\\Exception\\BadResponseException' => __DIR__ . '/..' . '/guzzle/http/Guzzle/Http/Exception/BadResponseException.php', - 'Guzzle\\Http\\Exception\\ClientErrorResponseException' => __DIR__ . '/..' . '/guzzle/http/Guzzle/Http/Exception/ClientErrorResponseException.php', - 'Guzzle\\Http\\Exception\\CouldNotRewindStreamException' => __DIR__ . '/..' . '/guzzle/http/Guzzle/Http/Exception/CouldNotRewindStreamException.php', - 'Guzzle\\Http\\Exception\\CurlException' => __DIR__ . '/..' . '/guzzle/http/Guzzle/Http/Exception/CurlException.php', - 'Guzzle\\Http\\Exception\\HttpException' => __DIR__ . '/..' . '/guzzle/http/Guzzle/Http/Exception/HttpException.php', - 'Guzzle\\Http\\Exception\\MultiTransferException' => __DIR__ . '/..' . '/guzzle/http/Guzzle/Http/Exception/MultiTransferException.php', - 'Guzzle\\Http\\Exception\\RequestException' => __DIR__ . '/..' . '/guzzle/http/Guzzle/Http/Exception/RequestException.php', - 'Guzzle\\Http\\Exception\\ServerErrorResponseException' => __DIR__ . '/..' . '/guzzle/http/Guzzle/Http/Exception/ServerErrorResponseException.php', - 'Guzzle\\Http\\Exception\\TooManyRedirectsException' => __DIR__ . '/..' . '/guzzle/http/Guzzle/Http/Exception/TooManyRedirectsException.php', - 'Guzzle\\Http\\IoEmittingEntityBody' => __DIR__ . '/..' . '/guzzle/http/Guzzle/Http/IoEmittingEntityBody.php', - 'Guzzle\\Http\\Message\\AbstractMessage' => __DIR__ . '/..' . '/guzzle/http/Guzzle/Http/Message/AbstractMessage.php', - 'Guzzle\\Http\\Message\\EntityEnclosingRequest' => __DIR__ . '/..' . '/guzzle/http/Guzzle/Http/Message/EntityEnclosingRequest.php', - 'Guzzle\\Http\\Message\\EntityEnclosingRequestInterface' => __DIR__ . '/..' . '/guzzle/http/Guzzle/Http/Message/EntityEnclosingRequestInterface.php', - 'Guzzle\\Http\\Message\\Header' => __DIR__ . '/..' . '/guzzle/http/Guzzle/Http/Message/Header.php', - 'Guzzle\\Http\\Message\\Header\\CacheControl' => __DIR__ . '/..' . '/guzzle/http/Guzzle/Http/Message/Header/CacheControl.php', - 'Guzzle\\Http\\Message\\Header\\HeaderCollection' => __DIR__ . '/..' . '/guzzle/http/Guzzle/Http/Message/Header/HeaderCollection.php', - 'Guzzle\\Http\\Message\\Header\\HeaderFactory' => __DIR__ . '/..' . '/guzzle/http/Guzzle/Http/Message/Header/HeaderFactory.php', - 'Guzzle\\Http\\Message\\Header\\HeaderFactoryInterface' => __DIR__ . '/..' . '/guzzle/http/Guzzle/Http/Message/Header/HeaderFactoryInterface.php', - 'Guzzle\\Http\\Message\\Header\\HeaderInterface' => __DIR__ . '/..' . '/guzzle/http/Guzzle/Http/Message/Header/HeaderInterface.php', - 'Guzzle\\Http\\Message\\Header\\Link' => __DIR__ . '/..' . '/guzzle/http/Guzzle/Http/Message/Header/Link.php', - 'Guzzle\\Http\\Message\\MessageInterface' => __DIR__ . '/..' . '/guzzle/http/Guzzle/Http/Message/MessageInterface.php', - 'Guzzle\\Http\\Message\\PostFile' => __DIR__ . '/..' . '/guzzle/http/Guzzle/Http/Message/PostFile.php', - 'Guzzle\\Http\\Message\\PostFileInterface' => __DIR__ . '/..' . '/guzzle/http/Guzzle/Http/Message/PostFileInterface.php', - 'Guzzle\\Http\\Message\\Request' => __DIR__ . '/..' . '/guzzle/http/Guzzle/Http/Message/Request.php', - 'Guzzle\\Http\\Message\\RequestFactory' => __DIR__ . '/..' . '/guzzle/http/Guzzle/Http/Message/RequestFactory.php', - 'Guzzle\\Http\\Message\\RequestFactoryInterface' => __DIR__ . '/..' . '/guzzle/http/Guzzle/Http/Message/RequestFactoryInterface.php', - 'Guzzle\\Http\\Message\\RequestInterface' => __DIR__ . '/..' . '/guzzle/http/Guzzle/Http/Message/RequestInterface.php', - 'Guzzle\\Http\\Message\\Response' => __DIR__ . '/..' . '/guzzle/http/Guzzle/Http/Message/Response.php', - 'Guzzle\\Http\\Mimetypes' => __DIR__ . '/..' . '/guzzle/http/Guzzle/Http/Mimetypes.php', - 'Guzzle\\Http\\QueryAggregator\\CommaAggregator' => __DIR__ . '/..' . '/guzzle/http/Guzzle/Http/QueryAggregator/CommaAggregator.php', - 'Guzzle\\Http\\QueryAggregator\\DuplicateAggregator' => __DIR__ . '/..' . '/guzzle/http/Guzzle/Http/QueryAggregator/DuplicateAggregator.php', - 'Guzzle\\Http\\QueryAggregator\\PhpAggregator' => __DIR__ . '/..' . '/guzzle/http/Guzzle/Http/QueryAggregator/PhpAggregator.php', - 'Guzzle\\Http\\QueryAggregator\\QueryAggregatorInterface' => __DIR__ . '/..' . '/guzzle/http/Guzzle/Http/QueryAggregator/QueryAggregatorInterface.php', - 'Guzzle\\Http\\QueryString' => __DIR__ . '/..' . '/guzzle/http/Guzzle/Http/QueryString.php', - 'Guzzle\\Http\\ReadLimitEntityBody' => __DIR__ . '/..' . '/guzzle/http/Guzzle/Http/ReadLimitEntityBody.php', - 'Guzzle\\Http\\RedirectPlugin' => __DIR__ . '/..' . '/guzzle/http/Guzzle/Http/RedirectPlugin.php', - 'Guzzle\\Http\\StaticClient' => __DIR__ . '/..' . '/guzzle/http/Guzzle/Http/StaticClient.php', - 'Guzzle\\Http\\Url' => __DIR__ . '/..' . '/guzzle/http/Guzzle/Http/Url.php', - 'Guzzle\\Parser\\Cookie\\CookieParser' => __DIR__ . '/..' . '/guzzle/parser/Guzzle/Parser/Cookie/CookieParser.php', - 'Guzzle\\Parser\\Cookie\\CookieParserInterface' => __DIR__ . '/..' . '/guzzle/parser/Guzzle/Parser/Cookie/CookieParserInterface.php', - 'Guzzle\\Parser\\Message\\AbstractMessageParser' => __DIR__ . '/..' . '/guzzle/parser/Guzzle/Parser/Message/AbstractMessageParser.php', - 'Guzzle\\Parser\\Message\\MessageParser' => __DIR__ . '/..' . '/guzzle/parser/Guzzle/Parser/Message/MessageParser.php', - 'Guzzle\\Parser\\Message\\MessageParserInterface' => __DIR__ . '/..' . '/guzzle/parser/Guzzle/Parser/Message/MessageParserInterface.php', - 'Guzzle\\Parser\\Message\\PeclHttpMessageParser' => __DIR__ . '/..' . '/guzzle/parser/Guzzle/Parser/Message/PeclHttpMessageParser.php', - 'Guzzle\\Parser\\ParserRegistry' => __DIR__ . '/..' . '/guzzle/parser/Guzzle/Parser/ParserRegistry.php', - 'Guzzle\\Parser\\UriTemplate\\PeclUriTemplate' => __DIR__ . '/..' . '/guzzle/parser/Guzzle/Parser/UriTemplate/PeclUriTemplate.php', - 'Guzzle\\Parser\\UriTemplate\\UriTemplate' => __DIR__ . '/..' . '/guzzle/parser/Guzzle/Parser/UriTemplate/UriTemplate.php', - 'Guzzle\\Parser\\UriTemplate\\UriTemplateInterface' => __DIR__ . '/..' . '/guzzle/parser/Guzzle/Parser/UriTemplate/UriTemplateInterface.php', - 'Guzzle\\Parser\\Url\\UrlParser' => __DIR__ . '/..' . '/guzzle/parser/Guzzle/Parser/Url/UrlParser.php', - 'Guzzle\\Parser\\Url\\UrlParserInterface' => __DIR__ . '/..' . '/guzzle/parser/Guzzle/Parser/Url/UrlParserInterface.php', - 'Guzzle\\Stream\\PhpStreamRequestFactory' => __DIR__ . '/..' . '/guzzle/stream/Guzzle/Stream/PhpStreamRequestFactory.php', - 'Guzzle\\Stream\\Stream' => __DIR__ . '/..' . '/guzzle/stream/Guzzle/Stream/Stream.php', - 'Guzzle\\Stream\\StreamInterface' => __DIR__ . '/..' . '/guzzle/stream/Guzzle/Stream/StreamInterface.php', - 'Guzzle\\Stream\\StreamRequestFactoryInterface' => __DIR__ . '/..' . '/guzzle/stream/Guzzle/Stream/StreamRequestFactoryInterface.php', + 'Guzzle\\Batch\\AbstractBatchDecorator' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Batch/AbstractBatchDecorator.php', + 'Guzzle\\Batch\\Batch' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Batch/Batch.php', + 'Guzzle\\Batch\\BatchBuilder' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Batch/BatchBuilder.php', + 'Guzzle\\Batch\\BatchClosureDivisor' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Batch/BatchClosureDivisor.php', + 'Guzzle\\Batch\\BatchClosureTransfer' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Batch/BatchClosureTransfer.php', + 'Guzzle\\Batch\\BatchCommandTransfer' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Batch/BatchCommandTransfer.php', + 'Guzzle\\Batch\\BatchDivisorInterface' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Batch/BatchDivisorInterface.php', + 'Guzzle\\Batch\\BatchInterface' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Batch/BatchInterface.php', + 'Guzzle\\Batch\\BatchRequestTransfer' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Batch/BatchRequestTransfer.php', + 'Guzzle\\Batch\\BatchSizeDivisor' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Batch/BatchSizeDivisor.php', + 'Guzzle\\Batch\\BatchTransferInterface' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Batch/BatchTransferInterface.php', + 'Guzzle\\Batch\\ExceptionBufferingBatch' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Batch/ExceptionBufferingBatch.php', + 'Guzzle\\Batch\\Exception\\BatchTransferException' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Batch/Exception/BatchTransferException.php', + 'Guzzle\\Batch\\FlushingBatch' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Batch/FlushingBatch.php', + 'Guzzle\\Batch\\HistoryBatch' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Batch/HistoryBatch.php', + 'Guzzle\\Batch\\NotifyingBatch' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Batch/NotifyingBatch.php', + 'Guzzle\\Cache\\AbstractCacheAdapter' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Cache/AbstractCacheAdapter.php', + 'Guzzle\\Cache\\CacheAdapterFactory' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Cache/CacheAdapterFactory.php', + 'Guzzle\\Cache\\CacheAdapterInterface' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Cache/CacheAdapterInterface.php', + 'Guzzle\\Cache\\ClosureCacheAdapter' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Cache/ClosureCacheAdapter.php', + 'Guzzle\\Cache\\DoctrineCacheAdapter' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Cache/DoctrineCacheAdapter.php', + 'Guzzle\\Cache\\NullCacheAdapter' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Cache/NullCacheAdapter.php', + 'Guzzle\\Cache\\Zf1CacheAdapter' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Cache/Zf1CacheAdapter.php', + 'Guzzle\\Cache\\Zf2CacheAdapter' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Cache/Zf2CacheAdapter.php', + 'Guzzle\\Common\\AbstractHasDispatcher' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Common/AbstractHasDispatcher.php', + 'Guzzle\\Common\\Collection' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Common/Collection.php', + 'Guzzle\\Common\\Event' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Common/Event.php', + 'Guzzle\\Common\\Exception\\BadMethodCallException' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Common/Exception/BadMethodCallException.php', + 'Guzzle\\Common\\Exception\\ExceptionCollection' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Common/Exception/ExceptionCollection.php', + 'Guzzle\\Common\\Exception\\GuzzleException' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Common/Exception/GuzzleException.php', + 'Guzzle\\Common\\Exception\\InvalidArgumentException' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Common/Exception/InvalidArgumentException.php', + 'Guzzle\\Common\\Exception\\RuntimeException' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Common/Exception/RuntimeException.php', + 'Guzzle\\Common\\Exception\\UnexpectedValueException' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Common/Exception/UnexpectedValueException.php', + 'Guzzle\\Common\\FromConfigInterface' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Common/FromConfigInterface.php', + 'Guzzle\\Common\\HasDispatcherInterface' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Common/HasDispatcherInterface.php', + 'Guzzle\\Common\\ToArrayInterface' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Common/ToArrayInterface.php', + 'Guzzle\\Common\\Version' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Common/Version.php', + 'Guzzle\\Http\\AbstractEntityBodyDecorator' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Http/AbstractEntityBodyDecorator.php', + 'Guzzle\\Http\\CachingEntityBody' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Http/CachingEntityBody.php', + 'Guzzle\\Http\\Client' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Http/Client.php', + 'Guzzle\\Http\\ClientInterface' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Http/ClientInterface.php', + 'Guzzle\\Http\\Curl\\CurlHandle' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Http/Curl/CurlHandle.php', + 'Guzzle\\Http\\Curl\\CurlMulti' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Http/Curl/CurlMulti.php', + 'Guzzle\\Http\\Curl\\CurlMultiInterface' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Http/Curl/CurlMultiInterface.php', + 'Guzzle\\Http\\Curl\\CurlMultiProxy' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Http/Curl/CurlMultiProxy.php', + 'Guzzle\\Http\\Curl\\CurlVersion' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Http/Curl/CurlVersion.php', + 'Guzzle\\Http\\Curl\\RequestMediator' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Http/Curl/RequestMediator.php', + 'Guzzle\\Http\\EntityBody' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Http/EntityBody.php', + 'Guzzle\\Http\\EntityBodyInterface' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Http/EntityBodyInterface.php', + 'Guzzle\\Http\\Exception\\BadResponseException' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Http/Exception/BadResponseException.php', + 'Guzzle\\Http\\Exception\\ClientErrorResponseException' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Http/Exception/ClientErrorResponseException.php', + 'Guzzle\\Http\\Exception\\CouldNotRewindStreamException' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Http/Exception/CouldNotRewindStreamException.php', + 'Guzzle\\Http\\Exception\\CurlException' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Http/Exception/CurlException.php', + 'Guzzle\\Http\\Exception\\HttpException' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Http/Exception/HttpException.php', + 'Guzzle\\Http\\Exception\\MultiTransferException' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Http/Exception/MultiTransferException.php', + 'Guzzle\\Http\\Exception\\RequestException' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Http/Exception/RequestException.php', + 'Guzzle\\Http\\Exception\\ServerErrorResponseException' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Http/Exception/ServerErrorResponseException.php', + 'Guzzle\\Http\\Exception\\TooManyRedirectsException' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Http/Exception/TooManyRedirectsException.php', + 'Guzzle\\Http\\IoEmittingEntityBody' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Http/IoEmittingEntityBody.php', + 'Guzzle\\Http\\Message\\AbstractMessage' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Http/Message/AbstractMessage.php', + 'Guzzle\\Http\\Message\\EntityEnclosingRequest' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Http/Message/EntityEnclosingRequest.php', + 'Guzzle\\Http\\Message\\EntityEnclosingRequestInterface' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Http/Message/EntityEnclosingRequestInterface.php', + 'Guzzle\\Http\\Message\\Header' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Http/Message/Header.php', + 'Guzzle\\Http\\Message\\Header\\CacheControl' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Http/Message/Header/CacheControl.php', + 'Guzzle\\Http\\Message\\Header\\HeaderCollection' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Http/Message/Header/HeaderCollection.php', + 'Guzzle\\Http\\Message\\Header\\HeaderFactory' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Http/Message/Header/HeaderFactory.php', + 'Guzzle\\Http\\Message\\Header\\HeaderFactoryInterface' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Http/Message/Header/HeaderFactoryInterface.php', + 'Guzzle\\Http\\Message\\Header\\HeaderInterface' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Http/Message/Header/HeaderInterface.php', + 'Guzzle\\Http\\Message\\Header\\Link' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Http/Message/Header/Link.php', + 'Guzzle\\Http\\Message\\MessageInterface' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Http/Message/MessageInterface.php', + 'Guzzle\\Http\\Message\\PostFile' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Http/Message/PostFile.php', + 'Guzzle\\Http\\Message\\PostFileInterface' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Http/Message/PostFileInterface.php', + 'Guzzle\\Http\\Message\\Request' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Http/Message/Request.php', + 'Guzzle\\Http\\Message\\RequestFactory' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Http/Message/RequestFactory.php', + 'Guzzle\\Http\\Message\\RequestFactoryInterface' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Http/Message/RequestFactoryInterface.php', + 'Guzzle\\Http\\Message\\RequestInterface' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Http/Message/RequestInterface.php', + 'Guzzle\\Http\\Message\\Response' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Http/Message/Response.php', + 'Guzzle\\Http\\Mimetypes' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Http/Mimetypes.php', + 'Guzzle\\Http\\QueryAggregator\\CommaAggregator' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Http/QueryAggregator/CommaAggregator.php', + 'Guzzle\\Http\\QueryAggregator\\DuplicateAggregator' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Http/QueryAggregator/DuplicateAggregator.php', + 'Guzzle\\Http\\QueryAggregator\\PhpAggregator' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Http/QueryAggregator/PhpAggregator.php', + 'Guzzle\\Http\\QueryAggregator\\QueryAggregatorInterface' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Http/QueryAggregator/QueryAggregatorInterface.php', + 'Guzzle\\Http\\QueryString' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Http/QueryString.php', + 'Guzzle\\Http\\ReadLimitEntityBody' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Http/ReadLimitEntityBody.php', + 'Guzzle\\Http\\RedirectPlugin' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Http/RedirectPlugin.php', + 'Guzzle\\Http\\StaticClient' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Http/StaticClient.php', + 'Guzzle\\Http\\Url' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Http/Url.php', + 'Guzzle\\Inflection\\Inflector' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Inflection/Inflector.php', + 'Guzzle\\Inflection\\InflectorInterface' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Inflection/InflectorInterface.php', + 'Guzzle\\Inflection\\MemoizingInflector' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Inflection/MemoizingInflector.php', + 'Guzzle\\Inflection\\PreComputedInflector' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Inflection/PreComputedInflector.php', + 'Guzzle\\Iterator\\AppendIterator' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Iterator/AppendIterator.php', + 'Guzzle\\Iterator\\ChunkedIterator' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Iterator/ChunkedIterator.php', + 'Guzzle\\Iterator\\FilterIterator' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Iterator/FilterIterator.php', + 'Guzzle\\Iterator\\MapIterator' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Iterator/MapIterator.php', + 'Guzzle\\Iterator\\MethodProxyIterator' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Iterator/MethodProxyIterator.php', + 'Guzzle\\Log\\AbstractLogAdapter' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Log/AbstractLogAdapter.php', + 'Guzzle\\Log\\ArrayLogAdapter' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Log/ArrayLogAdapter.php', + 'Guzzle\\Log\\ClosureLogAdapter' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Log/ClosureLogAdapter.php', + 'Guzzle\\Log\\LogAdapterInterface' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Log/LogAdapterInterface.php', + 'Guzzle\\Log\\MessageFormatter' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Log/MessageFormatter.php', + 'Guzzle\\Log\\MonologLogAdapter' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Log/MonologLogAdapter.php', + 'Guzzle\\Log\\PsrLogAdapter' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Log/PsrLogAdapter.php', + 'Guzzle\\Log\\Zf1LogAdapter' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Log/Zf1LogAdapter.php', + 'Guzzle\\Log\\Zf2LogAdapter' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Log/Zf2LogAdapter.php', + 'Guzzle\\Parser\\Cookie\\CookieParser' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Parser/Cookie/CookieParser.php', + 'Guzzle\\Parser\\Cookie\\CookieParserInterface' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Parser/Cookie/CookieParserInterface.php', + 'Guzzle\\Parser\\Message\\AbstractMessageParser' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Parser/Message/AbstractMessageParser.php', + 'Guzzle\\Parser\\Message\\MessageParser' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Parser/Message/MessageParser.php', + 'Guzzle\\Parser\\Message\\MessageParserInterface' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Parser/Message/MessageParserInterface.php', + 'Guzzle\\Parser\\Message\\PeclHttpMessageParser' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Parser/Message/PeclHttpMessageParser.php', + 'Guzzle\\Parser\\ParserRegistry' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Parser/ParserRegistry.php', + 'Guzzle\\Parser\\UriTemplate\\PeclUriTemplate' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Parser/UriTemplate/PeclUriTemplate.php', + 'Guzzle\\Parser\\UriTemplate\\UriTemplate' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Parser/UriTemplate/UriTemplate.php', + 'Guzzle\\Parser\\UriTemplate\\UriTemplateInterface' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Parser/UriTemplate/UriTemplateInterface.php', + 'Guzzle\\Parser\\Url\\UrlParser' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Parser/Url/UrlParser.php', + 'Guzzle\\Parser\\Url\\UrlParserInterface' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Parser/Url/UrlParserInterface.php', + 'Guzzle\\Plugin\\Async\\AsyncPlugin' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Plugin/Async/AsyncPlugin.php', + 'Guzzle\\Plugin\\Backoff\\AbstractBackoffStrategy' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Plugin/Backoff/AbstractBackoffStrategy.php', + 'Guzzle\\Plugin\\Backoff\\AbstractErrorCodeBackoffStrategy' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Plugin/Backoff/AbstractErrorCodeBackoffStrategy.php', + 'Guzzle\\Plugin\\Backoff\\BackoffLogger' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Plugin/Backoff/BackoffLogger.php', + 'Guzzle\\Plugin\\Backoff\\BackoffPlugin' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Plugin/Backoff/BackoffPlugin.php', + 'Guzzle\\Plugin\\Backoff\\BackoffStrategyInterface' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Plugin/Backoff/BackoffStrategyInterface.php', + 'Guzzle\\Plugin\\Backoff\\CallbackBackoffStrategy' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Plugin/Backoff/CallbackBackoffStrategy.php', + 'Guzzle\\Plugin\\Backoff\\ConstantBackoffStrategy' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Plugin/Backoff/ConstantBackoffStrategy.php', + 'Guzzle\\Plugin\\Backoff\\CurlBackoffStrategy' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Plugin/Backoff/CurlBackoffStrategy.php', + 'Guzzle\\Plugin\\Backoff\\ExponentialBackoffStrategy' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Plugin/Backoff/ExponentialBackoffStrategy.php', + 'Guzzle\\Plugin\\Backoff\\HttpBackoffStrategy' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Plugin/Backoff/HttpBackoffStrategy.php', + 'Guzzle\\Plugin\\Backoff\\LinearBackoffStrategy' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Plugin/Backoff/LinearBackoffStrategy.php', + 'Guzzle\\Plugin\\Backoff\\ReasonPhraseBackoffStrategy' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Plugin/Backoff/ReasonPhraseBackoffStrategy.php', + 'Guzzle\\Plugin\\Backoff\\TruncatedBackoffStrategy' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Plugin/Backoff/TruncatedBackoffStrategy.php', + 'Guzzle\\Plugin\\Cache\\CacheKeyProviderInterface' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Plugin/Cache/CacheKeyProviderInterface.php', + 'Guzzle\\Plugin\\Cache\\CachePlugin' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Plugin/Cache/CachePlugin.php', + 'Guzzle\\Plugin\\Cache\\CacheStorageInterface' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Plugin/Cache/CacheStorageInterface.php', + 'Guzzle\\Plugin\\Cache\\CallbackCanCacheStrategy' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Plugin/Cache/CallbackCanCacheStrategy.php', + 'Guzzle\\Plugin\\Cache\\CanCacheStrategyInterface' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Plugin/Cache/CanCacheStrategyInterface.php', + 'Guzzle\\Plugin\\Cache\\DefaultCacheKeyProvider' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Plugin/Cache/DefaultCacheKeyProvider.php', + 'Guzzle\\Plugin\\Cache\\DefaultCacheStorage' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Plugin/Cache/DefaultCacheStorage.php', + 'Guzzle\\Plugin\\Cache\\DefaultCanCacheStrategy' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Plugin/Cache/DefaultCanCacheStrategy.php', + 'Guzzle\\Plugin\\Cache\\DefaultRevalidation' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Plugin/Cache/DefaultRevalidation.php', + 'Guzzle\\Plugin\\Cache\\DenyRevalidation' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Plugin/Cache/DenyRevalidation.php', + 'Guzzle\\Plugin\\Cache\\RevalidationInterface' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Plugin/Cache/RevalidationInterface.php', + 'Guzzle\\Plugin\\Cache\\SkipRevalidation' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Plugin/Cache/SkipRevalidation.php', + 'Guzzle\\Plugin\\Cookie\\Cookie' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Plugin/Cookie/Cookie.php', + 'Guzzle\\Plugin\\Cookie\\CookieJar\\ArrayCookieJar' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Plugin/Cookie/CookieJar/ArrayCookieJar.php', + 'Guzzle\\Plugin\\Cookie\\CookieJar\\CookieJarInterface' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Plugin/Cookie/CookieJar/CookieJarInterface.php', + 'Guzzle\\Plugin\\Cookie\\CookieJar\\FileCookieJar' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Plugin/Cookie/CookieJar/FileCookieJar.php', + 'Guzzle\\Plugin\\Cookie\\CookiePlugin' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Plugin/Cookie/CookiePlugin.php', + 'Guzzle\\Plugin\\Cookie\\Exception\\InvalidCookieException' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Plugin/Cookie/Exception/InvalidCookieException.php', + 'Guzzle\\Plugin\\CurlAuth\\CurlAuthPlugin' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Plugin/CurlAuth/CurlAuthPlugin.php', + 'Guzzle\\Plugin\\ErrorResponse\\ErrorResponseExceptionInterface' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Plugin/ErrorResponse/ErrorResponseExceptionInterface.php', + 'Guzzle\\Plugin\\ErrorResponse\\ErrorResponsePlugin' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Plugin/ErrorResponse/ErrorResponsePlugin.php', + 'Guzzle\\Plugin\\ErrorResponse\\Exception\\ErrorResponseException' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Plugin/ErrorResponse/Exception/ErrorResponseException.php', + 'Guzzle\\Plugin\\History\\HistoryPlugin' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Plugin/History/HistoryPlugin.php', + 'Guzzle\\Plugin\\Log\\LogPlugin' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Plugin/Log/LogPlugin.php', + 'Guzzle\\Plugin\\Md5\\CommandContentMd5Plugin' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Plugin/Md5/CommandContentMd5Plugin.php', + 'Guzzle\\Plugin\\Md5\\Md5ValidatorPlugin' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Plugin/Md5/Md5ValidatorPlugin.php', + 'Guzzle\\Plugin\\Mock\\MockPlugin' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Plugin/Mock/MockPlugin.php', + 'Guzzle\\Plugin\\Oauth\\OauthPlugin' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Plugin/Oauth/OauthPlugin.php', + 'Guzzle\\Service\\AbstractConfigLoader' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/AbstractConfigLoader.php', + 'Guzzle\\Service\\Builder\\ServiceBuilder' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Builder/ServiceBuilder.php', + 'Guzzle\\Service\\Builder\\ServiceBuilderInterface' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Builder/ServiceBuilderInterface.php', + 'Guzzle\\Service\\Builder\\ServiceBuilderLoader' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Builder/ServiceBuilderLoader.php', + 'Guzzle\\Service\\CachingConfigLoader' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/CachingConfigLoader.php', + 'Guzzle\\Service\\Client' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Client.php', + 'Guzzle\\Service\\ClientInterface' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/ClientInterface.php', + 'Guzzle\\Service\\Command\\AbstractCommand' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Command/AbstractCommand.php', + 'Guzzle\\Service\\Command\\ClosureCommand' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Command/ClosureCommand.php', + 'Guzzle\\Service\\Command\\CommandInterface' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Command/CommandInterface.php', + 'Guzzle\\Service\\Command\\CreateResponseClassEvent' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Command/CreateResponseClassEvent.php', + 'Guzzle\\Service\\Command\\DefaultRequestSerializer' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Command/DefaultRequestSerializer.php', + 'Guzzle\\Service\\Command\\DefaultResponseParser' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Command/DefaultResponseParser.php', + 'Guzzle\\Service\\Command\\Factory\\AliasFactory' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Command/Factory/AliasFactory.php', + 'Guzzle\\Service\\Command\\Factory\\CompositeFactory' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Command/Factory/CompositeFactory.php', + 'Guzzle\\Service\\Command\\Factory\\ConcreteClassFactory' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Command/Factory/ConcreteClassFactory.php', + 'Guzzle\\Service\\Command\\Factory\\FactoryInterface' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Command/Factory/FactoryInterface.php', + 'Guzzle\\Service\\Command\\Factory\\MapFactory' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Command/Factory/MapFactory.php', + 'Guzzle\\Service\\Command\\Factory\\ServiceDescriptionFactory' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Command/Factory/ServiceDescriptionFactory.php', + 'Guzzle\\Service\\Command\\LocationVisitor\\Request\\AbstractRequestVisitor' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/AbstractRequestVisitor.php', + 'Guzzle\\Service\\Command\\LocationVisitor\\Request\\BodyVisitor' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/BodyVisitor.php', + 'Guzzle\\Service\\Command\\LocationVisitor\\Request\\HeaderVisitor' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/HeaderVisitor.php', + 'Guzzle\\Service\\Command\\LocationVisitor\\Request\\JsonVisitor' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/JsonVisitor.php', + 'Guzzle\\Service\\Command\\LocationVisitor\\Request\\PostFieldVisitor' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/PostFieldVisitor.php', + 'Guzzle\\Service\\Command\\LocationVisitor\\Request\\PostFileVisitor' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/PostFileVisitor.php', + 'Guzzle\\Service\\Command\\LocationVisitor\\Request\\QueryVisitor' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/QueryVisitor.php', + 'Guzzle\\Service\\Command\\LocationVisitor\\Request\\RequestVisitorInterface' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/RequestVisitorInterface.php', + 'Guzzle\\Service\\Command\\LocationVisitor\\Request\\ResponseBodyVisitor' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/ResponseBodyVisitor.php', + 'Guzzle\\Service\\Command\\LocationVisitor\\Request\\XmlVisitor' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/XmlVisitor.php', + 'Guzzle\\Service\\Command\\LocationVisitor\\Response\\AbstractResponseVisitor' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Response/AbstractResponseVisitor.php', + 'Guzzle\\Service\\Command\\LocationVisitor\\Response\\BodyVisitor' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Response/BodyVisitor.php', + 'Guzzle\\Service\\Command\\LocationVisitor\\Response\\HeaderVisitor' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Response/HeaderVisitor.php', + 'Guzzle\\Service\\Command\\LocationVisitor\\Response\\JsonVisitor' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Response/JsonVisitor.php', + 'Guzzle\\Service\\Command\\LocationVisitor\\Response\\ReasonPhraseVisitor' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Response/ReasonPhraseVisitor.php', + 'Guzzle\\Service\\Command\\LocationVisitor\\Response\\ResponseVisitorInterface' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Response/ResponseVisitorInterface.php', + 'Guzzle\\Service\\Command\\LocationVisitor\\Response\\StatusCodeVisitor' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Response/StatusCodeVisitor.php', + 'Guzzle\\Service\\Command\\LocationVisitor\\Response\\XmlVisitor' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Response/XmlVisitor.php', + 'Guzzle\\Service\\Command\\LocationVisitor\\VisitorFlyweight' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/VisitorFlyweight.php', + 'Guzzle\\Service\\Command\\OperationCommand' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Command/OperationCommand.php', + 'Guzzle\\Service\\Command\\OperationResponseParser' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Command/OperationResponseParser.php', + 'Guzzle\\Service\\Command\\RequestSerializerInterface' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Command/RequestSerializerInterface.php', + 'Guzzle\\Service\\Command\\ResponseClassInterface' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Command/ResponseClassInterface.php', + 'Guzzle\\Service\\Command\\ResponseParserInterface' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Command/ResponseParserInterface.php', + 'Guzzle\\Service\\ConfigLoaderInterface' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/ConfigLoaderInterface.php', + 'Guzzle\\Service\\Description\\Operation' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Description/Operation.php', + 'Guzzle\\Service\\Description\\OperationInterface' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Description/OperationInterface.php', + 'Guzzle\\Service\\Description\\Parameter' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Description/Parameter.php', + 'Guzzle\\Service\\Description\\SchemaFormatter' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Description/SchemaFormatter.php', + 'Guzzle\\Service\\Description\\SchemaValidator' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Description/SchemaValidator.php', + 'Guzzle\\Service\\Description\\ServiceDescription' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Description/ServiceDescription.php', + 'Guzzle\\Service\\Description\\ServiceDescriptionInterface' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Description/ServiceDescriptionInterface.php', + 'Guzzle\\Service\\Description\\ServiceDescriptionLoader' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Description/ServiceDescriptionLoader.php', + 'Guzzle\\Service\\Description\\ValidatorInterface' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Description/ValidatorInterface.php', + 'Guzzle\\Service\\Exception\\CommandException' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Exception/CommandException.php', + 'Guzzle\\Service\\Exception\\CommandTransferException' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Exception/CommandTransferException.php', + 'Guzzle\\Service\\Exception\\DescriptionBuilderException' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Exception/DescriptionBuilderException.php', + 'Guzzle\\Service\\Exception\\InconsistentClientTransferException' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Exception/InconsistentClientTransferException.php', + 'Guzzle\\Service\\Exception\\ResponseClassException' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Exception/ResponseClassException.php', + 'Guzzle\\Service\\Exception\\ServiceBuilderException' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Exception/ServiceBuilderException.php', + 'Guzzle\\Service\\Exception\\ServiceNotFoundException' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Exception/ServiceNotFoundException.php', + 'Guzzle\\Service\\Exception\\ValidationException' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Exception/ValidationException.php', + 'Guzzle\\Service\\Resource\\AbstractResourceIteratorFactory' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Resource/AbstractResourceIteratorFactory.php', + 'Guzzle\\Service\\Resource\\CompositeResourceIteratorFactory' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Resource/CompositeResourceIteratorFactory.php', + 'Guzzle\\Service\\Resource\\MapResourceIteratorFactory' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Resource/MapResourceIteratorFactory.php', + 'Guzzle\\Service\\Resource\\Model' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Resource/Model.php', + 'Guzzle\\Service\\Resource\\ResourceIterator' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Resource/ResourceIterator.php', + 'Guzzle\\Service\\Resource\\ResourceIteratorApplyBatched' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Resource/ResourceIteratorApplyBatched.php', + 'Guzzle\\Service\\Resource\\ResourceIteratorClassFactory' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Resource/ResourceIteratorClassFactory.php', + 'Guzzle\\Service\\Resource\\ResourceIteratorFactoryInterface' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Resource/ResourceIteratorFactoryInterface.php', + 'Guzzle\\Service\\Resource\\ResourceIteratorInterface' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Service/Resource/ResourceIteratorInterface.php', + 'Guzzle\\Stream\\PhpStreamRequestFactory' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Stream/PhpStreamRequestFactory.php', + 'Guzzle\\Stream\\Stream' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Stream/Stream.php', + 'Guzzle\\Stream\\StreamInterface' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Stream/StreamInterface.php', + 'Guzzle\\Stream\\StreamRequestFactoryInterface' => __DIR__ . '/..' . '/guzzle/guzzle/src/Guzzle/Stream/StreamRequestFactoryInterface.php', + 'Guzzle\\Tests\\Batch\\AbstractBatchDecoratorTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Batch/AbstractBatchDecoratorTest.php', + 'Guzzle\\Tests\\Batch\\BatchBuilderTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Batch/BatchBuilderTest.php', + 'Guzzle\\Tests\\Batch\\BatchClosureDivisorTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Batch/BatchClosureDivisorTest.php', + 'Guzzle\\Tests\\Batch\\BatchClosureTransferTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Batch/BatchClosureTransferTest.php', + 'Guzzle\\Tests\\Batch\\BatchCommandTransferTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Batch/BatchCommandTransferTest.php', + 'Guzzle\\Tests\\Batch\\BatchRequestTransferTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Batch/BatchRequestTransferTest.php', + 'Guzzle\\Tests\\Batch\\BatchSizeDivisorTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Batch/BatchSizeDivisorTest.php', + 'Guzzle\\Tests\\Batch\\BatchTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Batch/BatchTest.php', + 'Guzzle\\Tests\\Batch\\ExceptionBufferingBatchTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Batch/ExceptionBufferingBatchTest.php', + 'Guzzle\\Tests\\Batch\\FlushingBatchTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Batch/FlushingBatchTest.php', + 'Guzzle\\Tests\\Batch\\HistoryBatchTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Batch/HistoryBatchTest.php', + 'Guzzle\\Tests\\Batch\\NotifyingBatchTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Batch/NotifyingBatchTest.php', + 'Guzzle\\Tests\\Cache\\CacheAdapterFactoryTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Cache/CacheAdapterFactoryTest.php', + 'Guzzle\\Tests\\Cache\\CacheAdapterTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Cache/CacheAdapterTest.php', + 'Guzzle\\Tests\\Cache\\ClosureCacheAdapterTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Cache/ClosureCacheAdapterTest.php', + 'Guzzle\\Tests\\Cache\\Zf2CacheAdapterTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Cache/Zf2CacheAdapterTest.php', + 'Guzzle\\Tests\\Common\\AbstractHasAdapterTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Common/AbstractHasDispatcherTest.php', + 'Guzzle\\Tests\\Common\\Cache\\NullCacheAdapterTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Cache/NullCacheAdapterTest.php', + 'Guzzle\\Tests\\Common\\CollectionTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Common/CollectionTest.php', + 'Guzzle\\Tests\\Common\\EventTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Common/EventTest.php', + 'Guzzle\\Tests\\Common\\Exception\\BatchTransferExceptionTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Common/Exception/BatchTransferExceptionTest.php', + 'Guzzle\\Tests\\Common\\Exception\\ExceptionCollectionTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Common/Exception/ExceptionCollectionTest.php', + 'Guzzle\\Tests\\Common\\VersionTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Common/VersionTest.php', + 'Guzzle\\Tests\\GuzzleTestCase' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/GuzzleTestCase.php', + 'Guzzle\\Tests\\Http\\AbstractEntityBodyDecoratorTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Http/AbstractEntityBodyDecoratorTest.php', + 'Guzzle\\Tests\\Http\\CachingEntityBodyTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Http/CachingEntityBodyTest.php', + 'Guzzle\\Tests\\Http\\ClientTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Http/ClientTest.php', + 'Guzzle\\Tests\\Http\\CommaAggregatorTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Http/QueryAggregator/CommaAggregatorTest.php', + 'Guzzle\\Tests\\Http\\Curl\\CurlHandleTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Http/Curl/CurlHandleTest.php', + 'Guzzle\\Tests\\Http\\Curl\\CurlMultiProxyTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Http/Curl/CurlMultiProxyTest.php', + 'Guzzle\\Tests\\Http\\Curl\\CurlMultiTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Http/Curl/CurlMultiTest.php', + 'Guzzle\\Tests\\Http\\Curl\\CurlVersionTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Http/Curl/CurlVersionTest.php', + 'Guzzle\\Tests\\Http\\Curl\\RequestMediatorTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Http/Curl/RequestMediatorTest.php', + 'Guzzle\\Tests\\Http\\DuplicateAggregatorTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Http/QueryAggregator/DuplicateAggregatorTest.php', + 'Guzzle\\Tests\\Http\\EntityBodyTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Http/EntityBodyTest.php', + 'Guzzle\\Tests\\Http\\Exception\\CurlExceptionTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Http/Exception/CurlExceptionTest.php', + 'Guzzle\\Tests\\Http\\Exception\\ExceptionTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Http/Exception/ExceptionTest.php', + 'Guzzle\\Tests\\Http\\Exception\\MultiTransferExceptionTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Http/Exception/MultiTransferExceptionTest.php', + 'Guzzle\\Tests\\Http\\IoEmittingEntityBodyTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Http/IoEmittingEntityBodyTest.php', + 'Guzzle\\Tests\\Http\\Message\\AbstractMessageTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Http/Message/AbstractMessageTest.php', + 'Guzzle\\Tests\\Http\\Message\\EntityEnclosingRequestTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Http/Message/EntityEnclosingRequestTest.php', + 'Guzzle\\Tests\\Http\\Message\\HeaderComparison' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Http/Message/HeaderComparison.php', + 'Guzzle\\Tests\\Http\\Message\\HeaderTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Http/Message/HeaderTest.php', + 'Guzzle\\Tests\\Http\\Message\\Header\\HeaderFactoryTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Http/Message/Header/HeaderFactoryTest.php', + 'Guzzle\\Tests\\Http\\Message\\Header\\LinkTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Http/Message/Header/LinkTest.php', + 'Guzzle\\Tests\\Http\\Message\\HttpRequestFactoryTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Http/Message/RequestFactoryTest.php', + 'Guzzle\\Tests\\Http\\Message\\PostFileTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Http/Message/PostFileTest.php', + 'Guzzle\\Tests\\Http\\Message\\RequestTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Http/Message/RequestTest.php', + 'Guzzle\\Tests\\Http\\MimetypesTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Http/MimetypesTest.php', + 'Guzzle\\Tests\\Http\\PhpAggregatorTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Http/QueryAggregator/PhpAggregatorTest.php', + 'Guzzle\\Tests\\Http\\QueryStringTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Http/QueryStringTest.php', + 'Guzzle\\Tests\\Http\\ReadLimitEntityBodyTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Http/ReadLimitEntityBodyTest.php', + 'Guzzle\\Tests\\Http\\Server' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Http/Server.php', + 'Guzzle\\Tests\\Http\\UrlTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Http/UrlTest.php', + 'Guzzle\\Tests\\Inflection\\InflectorTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Inflection/InflectorTest.php', + 'Guzzle\\Tests\\Inflection\\MemoizingInflectorTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Inflection/MemoizingInflectorTest.php', + 'Guzzle\\Tests\\Inflection\\PreComputedInflectorTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Inflection/PreComputedInflectorTest.php', + 'Guzzle\\Tests\\Iterator\\AppendIteratorTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Iterator/AppendIteratorTest.php', + 'Guzzle\\Tests\\Iterator\\ChunkedIteratorTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Iterator/ChunkedIteratorTest.php', + 'Guzzle\\Tests\\Iterator\\FilterIteratorTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Iterator/FilterIteratorTest.php', + 'Guzzle\\Tests\\Iterator\\MapIteratorTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Iterator/MapIteratorTest.php', + 'Guzzle\\Tests\\Iterator\\MethodProxyIteratorTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Iterator/MethodProxyIteratorTest.php', + 'Guzzle\\Tests\\Log\\ArrayLogAdapterTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Log/ArrayLogAdapterTest.php', + 'Guzzle\\Tests\\Log\\ClosureLogAdapterTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Log/ClosureLogAdapterTest.php', + 'Guzzle\\Tests\\Log\\MessageFormatterTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Log/MessageFormatterTest.php', + 'Guzzle\\Tests\\Log\\PsrLogAdapterTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Log/PsrLogAdapterTest.php', + 'Guzzle\\Tests\\Log\\Zf2LogAdapterTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Log/Zf2LogAdapterTest.php', + 'Guzzle\\Tests\\Message\\HeaderComparisonTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Http/Message/HeaderComparisonTest.php', + 'Guzzle\\Tests\\Message\\ResponseTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Http/Message/ResponseTest.php', + 'Guzzle\\Tests\\Mock\\CustomResponseModel' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Mock/CustomResponseModel.php', + 'Guzzle\\Tests\\Mock\\ErrorResponseMock' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Mock/ErrorResponseMock.php', + 'Guzzle\\Tests\\Mock\\ExceptionMock' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Mock/ExceptionMock.php', + 'Guzzle\\Tests\\Mock\\MockMulti' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Mock/MockMulti.php', + 'Guzzle\\Tests\\Mock\\MockObserver' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Mock/MockObserver.php', + 'Guzzle\\Tests\\Mock\\MockSubject' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Mock/MockSubject.php', + 'Guzzle\\Tests\\Parser\\Cookie\\CookieParserProvider' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Parser/Cookie/CookieParserProvider.php', + 'Guzzle\\Tests\\Parser\\Cookie\\CookieParserTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Parser/Cookie/CookieParserTest.php', + 'Guzzle\\Tests\\Parser\\Message\\MessageParserProvider' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Parser/Message/MessageParserProvider.php', + 'Guzzle\\Tests\\Parser\\Message\\MessageParserTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Parser/Message/MessageParserTest.php', + 'Guzzle\\Tests\\Parser\\Message\\PeclHttpMessageParserTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Parser/Message/PeclHttpMessageParserTest.php', + 'Guzzle\\Tests\\Parser\\ParserRegistryTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Parser/ParserRegistryTest.php', + 'Guzzle\\Tests\\Parsers\\UriTemplate\\AbstractUriTemplateTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Parser/UriTemplate/AbstractUriTemplateTest.php', + 'Guzzle\\Tests\\Parsers\\UriTemplate\\PeclUriTemplateTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Parser/UriTemplate/PeclUriTemplateTest.php', + 'Guzzle\\Tests\\Parsers\\UriTemplate\\UriTemplateTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Parser/UriTemplate/UriTemplateTest.php', + 'Guzzle\\Tests\\Plugin\\Async\\AsyncPluginTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Async/AsyncPluginTest.php', + 'Guzzle\\Tests\\Plugin\\Backoff\\AbstractBackoffStrategyTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/AbstractBackoffStrategyTest.php', + 'Guzzle\\Tests\\Plugin\\Backoff\\BackoffLoggerTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/BackoffLoggerTest.php', + 'Guzzle\\Tests\\Plugin\\Backoff\\BackoffPluginTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/BackoffPluginTest.php', + 'Guzzle\\Tests\\Plugin\\Backoff\\CallbackBackoffStrategyTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/CallbackBackoffStrategyTest.php', + 'Guzzle\\Tests\\Plugin\\Backoff\\ConstantBackoffStrategyTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/ConstantBackoffStrategyTest.php', + 'Guzzle\\Tests\\Plugin\\Backoff\\CurlBackoffStrategyTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/CurlBackoffStrategyTest.php', + 'Guzzle\\Tests\\Plugin\\Backoff\\ExponentialBackoffStrategyTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/ExponentialBackoffStrategyTest.php', + 'Guzzle\\Tests\\Plugin\\Backoff\\HttpBackoffStrategyTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/HttpBackoffStrategyTest.php', + 'Guzzle\\Tests\\Plugin\\Backoff\\LinearBackoffStrategyTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/LinearBackoffStrategyTest.php', + 'Guzzle\\Tests\\Plugin\\Backoff\\ReasonPhraseBackoffStrategyTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/ReasonPhraseBackoffStrategyTest.php', + 'Guzzle\\Tests\\Plugin\\Backoff\\TruncatedBackoffStrategyTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/TruncatedBackoffStrategyTest.php', + 'Guzzle\\Tests\\Plugin\\Cache\\CachePluginTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cache/CachePluginTest.php', + 'Guzzle\\Tests\\Plugin\\Cache\\CallbackCanCacheStrategyTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cache/CallbackCanCacheStrategyTest.php', + 'Guzzle\\Tests\\Plugin\\Cache\\DefaultCacheStorageTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cache/DefaultCacheStorageTest.php', + 'Guzzle\\Tests\\Plugin\\Cache\\DefaultCanCacheStrategyTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cache/DefaultCanCacheStrategyTest.php', + 'Guzzle\\Tests\\Plugin\\Cache\\DefaultRevalidationTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cache/DefaultRevalidationTest.php', + 'Guzzle\\Tests\\Plugin\\Cache\\DenyRevalidationTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cache/DenyRevalidationTest.php', + 'Guzzle\\Tests\\Plugin\\Cache\\SkipRevalidationTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cache/SkipRevalidationTest.php', + 'Guzzle\\Tests\\Plugin\\Cookie\\CookieJar\\ArrayCookieJarTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cookie/CookieJar/ArrayCookieJarTest.php', + 'Guzzle\\Tests\\Plugin\\Cookie\\CookieJar\\FileCookieJarTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cookie/CookieJar/FileCookieJarTest.php', + 'Guzzle\\Tests\\Plugin\\Cookie\\CookiePluginTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cookie/CookiePluginTest.php', + 'Guzzle\\Tests\\Plugin\\Cookie\\CookieTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cookie/CookieTest.php', + 'Guzzle\\Tests\\Plugin\\CurlAuth\\CurlAuthPluginTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/CurlAuth/CurlAuthPluginTest.php', + 'Guzzle\\Tests\\Plugin\\ErrorResponse\\ErrorResponsePluginTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/ErrorResponse/ErrorResponsePluginTest.php', + 'Guzzle\\Tests\\Plugin\\History\\HistoryPluginTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/History/HistoryPluginTest.php', + 'Guzzle\\Tests\\Plugin\\Log\\LogPluginTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Log/LogPluginTest.php', + 'Guzzle\\Tests\\Plugin\\Md5\\CommandContentMd5PluginTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Md5/CommandContentMd5PluginTest.php', + 'Guzzle\\Tests\\Plugin\\Md5\\Md5ValidatorPluginTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Md5/Md5ValidatorPluginTest.php', + 'Guzzle\\Tests\\Plugin\\Mock\\MockPluginTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Mock/MockPluginTest.php', + 'Guzzle\\Tests\\Plugin\\Oauth\\OauthPluginTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Oauth/OauthPluginTest.php', + 'Guzzle\\Tests\\Plugin\\Redirect\\RedirectPluginTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Http/RedirectPluginTest.php', + 'Guzzle\\Tests\\Plugin\\Redirect\\StaticClientTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Http/StaticClientTest.php', + 'Guzzle\\Tests\\Service\\AbstractConfigLoaderTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Service/AbstractConfigLoaderTest.php', + 'Guzzle\\Tests\\Service\\Builder\\ServiceBuilderLoaderTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Builder/ServiceBuilderLoaderTest.php', + 'Guzzle\\Tests\\Service\\CachingConfigLoaderTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Service/CachingConfigLoaderTest.php', + 'Guzzle\\Tests\\Service\\ClientTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Service/ClientTest.php', + 'Guzzle\\Tests\\Service\\Command\\AbstractCommandTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/AbstractCommandTest.php', + 'Guzzle\\Tests\\Service\\Command\\AliasFactoryTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/Factory/AliasFactoryTest.php', + 'Guzzle\\Tests\\Service\\Command\\ClosureCommandTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/ClosureCommandTest.php', + 'Guzzle\\Tests\\Service\\Command\\CommandTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/CommandTest.php', + 'Guzzle\\Tests\\Service\\Command\\CompositeFactoryTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/Factory/CompositeFactoryTest.php', + 'Guzzle\\Tests\\Service\\Command\\ConcreteClassFactoryTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/Factory/ConcreteClassFactoryTest.php', + 'Guzzle\\Tests\\Service\\Command\\DefaultRequestSerializerTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/DefaultRequestSerializerTest.php', + 'Guzzle\\Tests\\Service\\Command\\DefaultResponseParserTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/DefaultResponseParserTest.php', + 'Guzzle\\Tests\\Service\\Command\\LocationVisitor\\Request\\AbstractVisitorTestCase' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Request/AbstractVisitorTestCase.php', + 'Guzzle\\Tests\\Service\\Command\\LocationVisitor\\Request\\BodyVisitorTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Request/BodyVisitorTest.php', + 'Guzzle\\Tests\\Service\\Command\\LocationVisitor\\Request\\HeaderVisitorTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Request/HeaderVisitorTest.php', + 'Guzzle\\Tests\\Service\\Command\\LocationVisitor\\Request\\JsonVisitorTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Request/JsonVisitorTest.php', + 'Guzzle\\Tests\\Service\\Command\\LocationVisitor\\Request\\PostFieldVisitorTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Request/PostFieldVisitorTest.php', + 'Guzzle\\Tests\\Service\\Command\\LocationVisitor\\Request\\PostFileVisitorTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Request/PostFileVisitorTest.php', + 'Guzzle\\Tests\\Service\\Command\\LocationVisitor\\Request\\QueryVisitorTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Request/QueryVisitorTest.php', + 'Guzzle\\Tests\\Service\\Command\\LocationVisitor\\Request\\ResponseBodyVisitorTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Request/ResponseBodyVisitorTest.php', + 'Guzzle\\Tests\\Service\\Command\\LocationVisitor\\Request\\XmlVisitorTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Request/XmlVisitorTest.php', + 'Guzzle\\Tests\\Service\\Command\\LocationVisitor\\Response\\AbstractResponseVisitorTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Response/AbstractResponseVisitorTest.php', + 'Guzzle\\Tests\\Service\\Command\\LocationVisitor\\Response\\BodyVisitorTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Response/BodyVisitorTest.php', + 'Guzzle\\Tests\\Service\\Command\\LocationVisitor\\Response\\HeaderVisitorTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Response/HeaderVisitorTest.php', + 'Guzzle\\Tests\\Service\\Command\\LocationVisitor\\Response\\JsonVisitorTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Response/JsonVisitorTest.php', + 'Guzzle\\Tests\\Service\\Command\\LocationVisitor\\Response\\ReasonPhraseVisitorTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Response/ReasonPhraseVisitorTest.php', + 'Guzzle\\Tests\\Service\\Command\\LocationVisitor\\Response\\StatusCodeVisitorTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Response/StatusCodeVisitorTest.php', + 'Guzzle\\Tests\\Service\\Command\\LocationVisitor\\Response\\XmlVisitorTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Response/XmlVisitorTest.php', + 'Guzzle\\Tests\\Service\\Command\\MapFactoryTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/Factory/MapFactoryTest.php', + 'Guzzle\\Tests\\Service\\Command\\OperationCommandTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/OperationCommandTest.php', + 'Guzzle\\Tests\\Service\\Command\\OperationResponseParserTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/OperationResponseParserTest.php', + 'Guzzle\\Tests\\Service\\Command\\ServiceDescriptionFactoryTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/Factory/ServiceDescriptionFactoryTest.php', + 'Guzzle\\Tests\\Service\\Command\\VisitorFlyweightTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/VisitorFlyweightTest.php', + 'Guzzle\\Tests\\Service\\Description\\OperationTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Description/OperationTest.php', + 'Guzzle\\Tests\\Service\\Description\\ParameterTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Description/ParameterTest.php', + 'Guzzle\\Tests\\Service\\Description\\SchemaFormatterTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Description/SchemaFormatterTest.php', + 'Guzzle\\Tests\\Service\\Description\\SchemaValidatorTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Description/SchemaValidatorTest.php', + 'Guzzle\\Tests\\Service\\Description\\ServiceDescriptionLoaderTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Description/ServiceDescriptionLoaderTest.php', + 'Guzzle\\Tests\\Service\\Description\\ServiceDescriptionTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Description/ServiceDescriptionTest.php', + 'Guzzle\\Tests\\Service\\Exception\\CommandTransferExceptionTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Exception/CommandTransferExceptionTest.php', + 'Guzzle\\Tests\\Service\\Exception\\InconsistentClientTransferExceptionTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Exception/InconsistentClientTransferExceptionTest.php', + 'Guzzle\\Tests\\Service\\Exception\\ValidationExceptionTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Exception/ValidationExceptionTest.php', + 'Guzzle\\Tests\\Service\\Mock\\Command\\IterableCommand' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Mock/Command/IterableCommand.php', + 'Guzzle\\Tests\\Service\\Mock\\Command\\MockCommand' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Mock/Command/MockCommand.php', + 'Guzzle\\Tests\\Service\\Mock\\Command\\OtherCommand' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Mock/Command/OtherCommand.php', + 'Guzzle\\Tests\\Service\\Mock\\Command\\Sub\\Sub' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Mock/Command/Sub/Sub.php', + 'Guzzle\\Tests\\Service\\Mock\\MockClient' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Mock/MockClient.php', + 'Guzzle\\Tests\\Service\\Mock\\Model\\MockCommandIterator' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Mock/Model/MockCommandIterator.php', + 'Guzzle\\Tests\\Service\\Resource\\CompositeResourceIteratorFactoryTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Resource/CompositeResourceIteratorFactoryTest.php', + 'Guzzle\\Tests\\Service\\Resource\\MapResourceIteratorFactoryTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Resource/MapResourceIteratorFactoryTest.php', + 'Guzzle\\Tests\\Service\\Resource\\ModelTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Resource/ModelTest.php', + 'Guzzle\\Tests\\Service\\Resource\\ResourceIteratorClassFactoryTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Resource/ResourceIteratorClassFactoryTest.php', + 'Guzzle\\Tests\\Service\\Resource\\ResourceIteratorTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Resource/ResourceIteratorTest.php', + 'Guzzle\\Tests\\Service\\ServiceBuilderTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Service/Builder/ServiceBuilderTest.php', + 'Guzzle\\Tests\\Stream\\PhpStreamRequestFactoryTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Stream/PhpStreamRequestFactoryTest.php', + 'Guzzle\\Tests\\Stream\\StreamTest' => __DIR__ . '/..' . '/guzzle/guzzle/tests/Guzzle/Tests/Stream/StreamTest.php', 'ID3Parser\\ID3Parser' => __DIR__ . '/..' . '/lukasreschke/id3parser/src/ID3Parser.php', 'ID3Parser\\getID3\\Tags\\getid3_id3v1' => __DIR__ . '/..' . '/lukasreschke/id3parser/src/getID3/Tags/getid3_id3v1.php', 'ID3Parser\\getID3\\Tags\\getid3_id3v2' => __DIR__ . '/..' . '/lukasreschke/id3parser/src/getID3/Tags/getid3_id3v2.php', @@ -945,6 +1271,20 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'Icewind\\Streams\\PathWrapper' => __DIR__ . '/..' . '/icewind/streams/src/PathWrapper.php', 'Icewind\\Streams\\RetryWrapper' => __DIR__ . '/..' . '/icewind/streams/src/RetryWrapper.php', 'Icewind\\Streams\\SeekableWrapper' => __DIR__ . '/..' . '/icewind/streams/src/SeekableWrapper.php', + 'Icewind\\Streams\\Tests\\CallbackWrapper' => __DIR__ . '/..' . '/icewind/streams/tests/CallbackWrapper.php', + 'Icewind\\Streams\\Tests\\DirectoryFilter' => __DIR__ . '/..' . '/icewind/streams/tests/DirectoryFilter.php', + 'Icewind\\Streams\\Tests\\DirectoryWrapper' => __DIR__ . '/..' . '/icewind/streams/tests/DirectoryWrapper.php', + 'Icewind\\Streams\\Tests\\DirectoryWrapperDummy' => __DIR__ . '/..' . '/icewind/streams/tests/DirectoryWrapper.php', + 'Icewind\\Streams\\Tests\\DirectoryWrapperNull' => __DIR__ . '/..' . '/icewind/streams/tests/DirectoryWrapper.php', + 'Icewind\\Streams\\Tests\\FailWrapper' => __DIR__ . '/..' . '/icewind/streams/tests/RetryWrapper.php', + 'Icewind\\Streams\\Tests\\IteratorDirectory' => __DIR__ . '/..' . '/icewind/streams/tests/IteratorDirectory.php', + 'Icewind\\Streams\\Tests\\NullWrapper' => __DIR__ . '/..' . '/icewind/streams/tests/NullWrapper.php', + 'Icewind\\Streams\\Tests\\PartialWrapper' => __DIR__ . '/..' . '/icewind/streams/tests/RetryWrapper.php', + 'Icewind\\Streams\\Tests\\PathWrapper' => __DIR__ . '/..' . '/icewind/streams/tests/PathWrapper.php', + 'Icewind\\Streams\\Tests\\RetryWrapper' => __DIR__ . '/..' . '/icewind/streams/tests/RetryWrapper.php', + 'Icewind\\Streams\\Tests\\SeekableWrapper' => __DIR__ . '/..' . '/icewind/streams/tests/SeekableWrapper.php', + 'Icewind\\Streams\\Tests\\UrlCallBack' => __DIR__ . '/..' . '/icewind/streams/tests/UrlCallBack.php', + 'Icewind\\Streams\\Tests\\Wrapper' => __DIR__ . '/..' . '/icewind/streams/tests/Wrapper.php', 'Icewind\\Streams\\Url' => __DIR__ . '/..' . '/icewind/streams/src/Url.php', 'Icewind\\Streams\\UrlCallback' => __DIR__ . '/..' . '/icewind/streams/src/UrlCallBack.php', 'Icewind\\Streams\\Wrapper' => __DIR__ . '/..' . '/icewind/streams/src/Wrapper.php', @@ -1027,6 +1367,10 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'OpenCloud\\Autoscale\\Resource\\ScalingPolicy' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Autoscale/Resource/ScalingPolicy.php', 'OpenCloud\\Autoscale\\Resource\\Webhook' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Autoscale/Resource/Webhook.php', 'OpenCloud\\Autoscale\\Service' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Autoscale/Service.php', + 'OpenCloud\\CDN\\Resource\\Flavor' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/CDN/Resource/Flavor.php', + 'OpenCloud\\CDN\\Resource\\Service' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/CDN/Resource/Service.php', + 'OpenCloud\\CDN\\Service' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/CDN/Service.php', + 'OpenCloud\\CloudMonitoring\\Collection\\MonitoringIterator' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Collection/MonitoringIterator.php', 'OpenCloud\\CloudMonitoring\\Exception\\AgentException' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Exception/AgentException.php', 'OpenCloud\\CloudMonitoring\\Exception\\AlarmException' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Exception/AlarmException.php', 'OpenCloud\\CloudMonitoring\\Exception\\CheckException' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Exception/CheckException.php', @@ -1051,14 +1395,16 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'OpenCloud\\CloudMonitoring\\Resource\\CheckType' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/CheckType.php', 'OpenCloud\\CloudMonitoring\\Resource\\Entity' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/Entity.php', 'OpenCloud\\CloudMonitoring\\Resource\\Metric' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/Metric.php', + 'OpenCloud\\CloudMonitoring\\Resource\\MetricDataPoint' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/MetricDataPoint.php', 'OpenCloud\\CloudMonitoring\\Resource\\Notification' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/Notification.php', 'OpenCloud\\CloudMonitoring\\Resource\\NotificationHistory' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/NotificationHistory.php', 'OpenCloud\\CloudMonitoring\\Resource\\NotificationPlan' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/NotificationPlan.php', 'OpenCloud\\CloudMonitoring\\Resource\\NotificationType' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/NotificationType.php', - 'OpenCloud\\CloudMonitoring\\Resource\\ReadonlyResource' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/ReadOnlyResource.php', + 'OpenCloud\\CloudMonitoring\\Resource\\ReadOnlyResource' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/ReadOnlyResource.php', 'OpenCloud\\CloudMonitoring\\Resource\\View' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/View.php', 'OpenCloud\\CloudMonitoring\\Resource\\Zone' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/Zone.php', 'OpenCloud\\CloudMonitoring\\Service' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Service.php', + 'OpenCloud\\Common\\ArrayAccess' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Common/ArrayAccess.php', 'OpenCloud\\Common\\Base' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Common/Base.php', 'OpenCloud\\Common\\Collection' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Common/Collection.php', 'OpenCloud\\Common\\Collection\\ArrayCollection' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Common/Collection/ArrayCollection.php', @@ -1075,6 +1421,8 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'OpenCloud\\Common\\Exceptions\\AsyncTimeoutError' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/AsyncTimeoutError.php', 'OpenCloud\\Common\\Exceptions\\AttributeError' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/AttributeError.php', 'OpenCloud\\Common\\Exceptions\\AuthenticationError' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/AuthenticationError.php', + 'OpenCloud\\Common\\Exceptions\\BackupInstanceError' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/BackupInstanceError.php', + 'OpenCloud\\Common\\Exceptions\\BackupNameError' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/BackupNameError.php', 'OpenCloud\\Common\\Exceptions\\BaseException' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/BaseException.php', 'OpenCloud\\Common\\Exceptions\\CdnError' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/CdnError.php', 'OpenCloud\\Common\\Exceptions\\CdnHttpError' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/CdnHttpError.php', @@ -1098,12 +1446,15 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'OpenCloud\\Common\\Exceptions\\DeleteError' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/DeleteError.php', 'OpenCloud\\Common\\Exceptions\\DocumentError' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/DocumentError.php', 'OpenCloud\\Common\\Exceptions\\DomainError' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/DomainError.php', + 'OpenCloud\\Common\\Exceptions\\DomainNotFoundException' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/DomainNotFoundException.php', 'OpenCloud\\Common\\Exceptions\\EmptyResponseError' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/EmptyResponseError.php', 'OpenCloud\\Common\\Exceptions\\EndpointError' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/EndpointError.php', 'OpenCloud\\Common\\Exceptions\\FlavorError' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/FlavorError.php', + 'OpenCloud\\Common\\Exceptions\\ForbiddenOperationException' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/ForbiddenOperationException.php', 'OpenCloud\\Common\\Exceptions\\HttpError' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/HttpError.php', 'OpenCloud\\Common\\Exceptions\\HttpForbiddenError' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/HttpForbiddenError.php', 'OpenCloud\\Common\\Exceptions\\HttpOverLimitError' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/HttpOverLimitError.php', + 'OpenCloud\\Common\\Exceptions\\HttpResponseException' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/HttpResponseException.php', 'OpenCloud\\Common\\Exceptions\\HttpRetryError' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/HttpRetryError.php', 'OpenCloud\\Common\\Exceptions\\HttpTimeoutError' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/HttpTimeoutError.php', 'OpenCloud\\Common\\Exceptions\\HttpUnauthorizedError' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/HttpUnauthorizedError.php', @@ -1122,6 +1473,7 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'OpenCloud\\Common\\Exceptions\\InvalidIpTypeError' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/InvalidIpTypeError.php', 'OpenCloud\\Common\\Exceptions\\InvalidParameterError' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/InvalidParameterError.php', 'OpenCloud\\Common\\Exceptions\\InvalidRequestError' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/InvalidRequestError.php', + 'OpenCloud\\Common\\Exceptions\\InvalidTemplateError' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/InvalidTemplateError.php', 'OpenCloud\\Common\\Exceptions\\JsonError' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/JsonError.php', 'OpenCloud\\Common\\Exceptions\\LoggingException' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/LoggingException.php', 'OpenCloud\\Common\\Exceptions\\MetadataCreateError' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/MetadataCreateError.php', @@ -1147,6 +1499,7 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'OpenCloud\\Common\\Exceptions\\RebuildError' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/RebuildError.php', 'OpenCloud\\Common\\Exceptions\\RecordTypeError' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/RecordTypeError.php', 'OpenCloud\\Common\\Exceptions\\ResourceBucketException' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/ResourceBucketException.php', + 'OpenCloud\\Common\\Exceptions\\ResourceNotFoundException' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/ResourceNotFoundException.php', 'OpenCloud\\Common\\Exceptions\\RuntimeException' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/RuntimeException.php', 'OpenCloud\\Common\\Exceptions\\ServerActionError' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/ServerActionError.php', 'OpenCloud\\Common\\Exceptions\\ServerCreateError' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/ServerCreateError.php', @@ -1178,12 +1531,13 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'OpenCloud\\Common\\Http\\Message\\Formatter' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Common/Http/Message/Formatter.php', 'OpenCloud\\Common\\Http\\Message\\RequestSubscriber' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Common/Http/Message/RequestSubscriber.php', 'OpenCloud\\Common\\Lang' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Common/Lang.php', - 'OpenCloud\\Common\\Log\\AbstractLogger' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Common/Log/AbstractLogger.php', - 'OpenCloud\\Common\\Log\\LogLevel' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Common/Log/LogLevel.php', 'OpenCloud\\Common\\Log\\Logger' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Common/Log/Logger.php', - 'OpenCloud\\Common\\Log\\LoggerInterface' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Common/Log/LoggerInterface.php', 'OpenCloud\\Common\\Metadata' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Common/Metadata.php', 'OpenCloud\\Common\\PersistentObject' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Common/PersistentObject.php', + 'OpenCloud\\Common\\Resource\\BaseResource' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Common/Resource/BaseResource.php', + 'OpenCloud\\Common\\Resource\\NovaResource' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Common/Resource/NovaResource.php', + 'OpenCloud\\Common\\Resource\\PersistentResource' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Common/Resource/PersistentResource.php', + 'OpenCloud\\Common\\Resource\\ReadOnlyResource' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Common/Resource/ReadOnlyResource.php', 'OpenCloud\\Common\\Service\\AbstractService' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Common/Service/AbstractService.php', 'OpenCloud\\Common\\Service\\Catalog' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Common/Service/Catalog.php', 'OpenCloud\\Common\\Service\\CatalogItem' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Common/Service/CatalogItem.php', @@ -1205,14 +1559,19 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'OpenCloud\\Compute\\Resource\\VolumeAttachment' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Compute/Resource/VolumeAttachment.php', 'OpenCloud\\Compute\\Service' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Compute/Service.php', 'OpenCloud\\DNS\\Collection\\DnsIterator' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/DNS/Collection/DnsIterator.php', + 'OpenCloud\\DNS\\Resource\\AbstractResource' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/DNS/Resource/AbstractResource.php', 'OpenCloud\\DNS\\Resource\\AsyncResponse' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/DNS/Resource/AsyncResponse.php', 'OpenCloud\\DNS\\Resource\\Domain' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/DNS/Resource/Domain.php', - 'OpenCloud\\DNS\\Resource\\Object' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/DNS/Resource/Object.php', + 'OpenCloud\\DNS\\Resource\\HasPtrRecordsInterface' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/DNS/Resource/HasPtrRecordsInterface.php', 'OpenCloud\\DNS\\Resource\\PtrRecord' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/DNS/Resource/PtrRecord.php', 'OpenCloud\\DNS\\Resource\\Record' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/DNS/Resource/Record.php', 'OpenCloud\\DNS\\Resource\\Subdomain' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/DNS/Resource/Subdomain.php', 'OpenCloud\\DNS\\Service' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/DNS/Service.php', + 'OpenCloud\\Database\\Resource\\Backup' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Database/Resource/Backup.php', + 'OpenCloud\\Database\\Resource\\Configuration' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Database/Resource/Configuration.php', 'OpenCloud\\Database\\Resource\\Database' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Database/Resource/Database.php', + 'OpenCloud\\Database\\Resource\\Datastore' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Database/Resource/Datastore.php', + 'OpenCloud\\Database\\Resource\\DatastoreVersion' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Database/Resource/DatastoreVersion.php', 'OpenCloud\\Database\\Resource\\Instance' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Database/Resource/Instance.php', 'OpenCloud\\Database\\Resource\\User' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Database/Resource/User.php', 'OpenCloud\\Database\\Service' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Database/Service.php', @@ -1222,10 +1581,30 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'OpenCloud\\Identity\\Resource\\Token' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Identity/Resource/Token.php', 'OpenCloud\\Identity\\Resource\\User' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Identity/Resource/User.php', 'OpenCloud\\Identity\\Service' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Identity/Service.php', + 'OpenCloud\\Image\\Enum\\Document' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Image/Enum/Document.php', + 'OpenCloud\\Image\\Enum\\MemberStatus' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Image/Enum/MemberStatus.php', + 'OpenCloud\\Image\\Enum\\OperationType' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Image/Enum/OperationType.php', + 'OpenCloud\\Image\\Enum\\Schema' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Image/Enum/Schema.php', + 'OpenCloud\\Image\\Resource\\AbstractSchemaResource' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Image/Resource/AbstractSchemaResource.php', + 'OpenCloud\\Image\\Resource\\Image' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Image/Resource/Image.php', + 'OpenCloud\\Image\\Resource\\ImageInterface' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Image/Resource/ImageInterface.php', + 'OpenCloud\\Image\\Resource\\JsonPatch\\Document' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Image/Resource/JsonPatch/Document.php', + 'OpenCloud\\Image\\Resource\\JsonPatch\\Encoder' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Image/Resource/JsonPatch/Encoder.php', + 'OpenCloud\\Image\\Resource\\JsonPatch\\Operation' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Image/Resource/JsonPatch/Operation.php', + 'OpenCloud\\Image\\Resource\\Member' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Image/Resource/Member.php', + 'OpenCloud\\Image\\Resource\\Schema\\AbstractSchemaItem' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Image/Resource/Schema/AbstractSchemaItem.php', + 'OpenCloud\\Image\\Resource\\Schema\\Property' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Image/Resource/Schema/Property.php', + 'OpenCloud\\Image\\Resource\\Schema\\Schema' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Image/Resource/Schema/Schema.php', + 'OpenCloud\\Image\\Service' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Image/Service.php', + 'OpenCloud\\LoadBalancer\\Collection\\LoadBalancerIterator' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Collection/LoadBalancerIterator.php', + 'OpenCloud\\LoadBalancer\\Enum\\IpType' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Enum/IpType.php', + 'OpenCloud\\LoadBalancer\\Enum\\NodeCondition' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Enum/NodeCondition.php', + 'OpenCloud\\LoadBalancer\\Enum\\NodeType' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Enum/NodeType.php', + 'OpenCloud\\LoadBalancer\\Resource\\AbstractResource' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/AbstractResource.php', 'OpenCloud\\LoadBalancer\\Resource\\Access' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/Access.php', 'OpenCloud\\LoadBalancer\\Resource\\Algorithm' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/Algorithm.php', 'OpenCloud\\LoadBalancer\\Resource\\AllowedDomain' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/AllowedDomain.php', - 'OpenCloud\\LoadBalancer\\Resource\\BillableLoadBalancer' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/BillableLoadBalancer.php', + 'OpenCloud\\LoadBalancer\\Resource\\CertificateMapping' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/CertificateMapping.php', 'OpenCloud\\LoadBalancer\\Resource\\ConnectionLogging' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/ConnectionLogging.php', 'OpenCloud\\LoadBalancer\\Resource\\ConnectionThrottle' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/ConnectionThrottle.php', 'OpenCloud\\LoadBalancer\\Resource\\ContentCaching' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/ContentCaching.php', @@ -1235,21 +1614,30 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'OpenCloud\\LoadBalancer\\Resource\\Metadata' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/Metadata.php', 'OpenCloud\\LoadBalancer\\Resource\\Node' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/Node.php', 'OpenCloud\\LoadBalancer\\Resource\\NodeEvent' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/NodeEvent.php', + 'OpenCloud\\LoadBalancer\\Resource\\NonIdUriResource' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/NonIdUriResource.php', 'OpenCloud\\LoadBalancer\\Resource\\Protocol' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/Protocol.php', - 'OpenCloud\\LoadBalancer\\Resource\\Readonly' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/Readonly.php', + 'OpenCloud\\LoadBalancer\\Resource\\ReadOnlyResource' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/ReadOnlyResource.php', 'OpenCloud\\LoadBalancer\\Resource\\SSLTermination' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/SSLTermination.php', 'OpenCloud\\LoadBalancer\\Resource\\SessionPersistence' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/SessionPersistence.php', 'OpenCloud\\LoadBalancer\\Resource\\Stats' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/Stats.php', - 'OpenCloud\\LoadBalancer\\Resource\\SubResource' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/SubResource.php', - 'OpenCloud\\LoadBalancer\\Resource\\Usage' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/Usage.php', + 'OpenCloud\\LoadBalancer\\Resource\\UsageRecord' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/UsageRecord.php', 'OpenCloud\\LoadBalancer\\Resource\\VirtualIp' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/VirtualIp.php', 'OpenCloud\\LoadBalancer\\Service' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Service.php', + 'OpenCloud\\Networking\\Resource\\Network' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Networking/Resource/Network.php', + 'OpenCloud\\Networking\\Resource\\NetworkInterface' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Networking/Resource/NetworkInterface.php', + 'OpenCloud\\Networking\\Resource\\Port' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Networking/Resource/Port.php', + 'OpenCloud\\Networking\\Resource\\SecurityGroup' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Networking/Resource/SecurityGroup.php', + 'OpenCloud\\Networking\\Resource\\SecurityGroupRule' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Networking/Resource/SecurityGroupRule.php', + 'OpenCloud\\Networking\\Resource\\Subnet' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Networking/Resource/Subnet.php', + 'OpenCloud\\Networking\\Service' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Networking/Service.php', 'OpenCloud\\ObjectStore\\AbstractService' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/AbstractService.php', 'OpenCloud\\ObjectStore\\CDNService' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/CDNService.php', 'OpenCloud\\ObjectStore\\Constants\\Header' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Constants/Header.php', 'OpenCloud\\ObjectStore\\Constants\\UrlType' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Constants/UrlType.php', + 'OpenCloud\\ObjectStore\\Enum\\ReturnType' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Enum/ReturnType.php', 'OpenCloud\\ObjectStore\\Exception\\BulkOperationException' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Exception/BulkOperationException.php', 'OpenCloud\\ObjectStore\\Exception\\ContainerException' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Exception/ContainerException.php', + 'OpenCloud\\ObjectStore\\Exception\\ObjectNotEmptyException' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Exception/ObjectNotEmptyException.php', 'OpenCloud\\ObjectStore\\Exception\\ObjectNotFoundException' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Exception/ObjectNotFoundException.php', 'OpenCloud\\ObjectStore\\Exception\\StreamException' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Exception/StreamException.php', 'OpenCloud\\ObjectStore\\Exception\\UploadException' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Exception/UploadException.php', @@ -1270,9 +1658,13 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'OpenCloud\\ObjectStore\\Upload\\TransferPart' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Upload/TransferPart.php', 'OpenCloud\\ObjectStore\\Upload\\TransferState' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Upload/TransferState.php', 'OpenCloud\\OpenStack' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/OpenStack.php', - 'OpenCloud\\Orchestration\\Resource' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Orchestration/Resource.php', + 'OpenCloud\\Orchestration\\Resource\\BuildInfo' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Orchestration/Resource/BuildInfo.php', + 'OpenCloud\\Orchestration\\Resource\\Event' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Orchestration/Resource/Event.php', + 'OpenCloud\\Orchestration\\Resource\\Resource' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Orchestration/Resource/Resource.php', + 'OpenCloud\\Orchestration\\Resource\\ResourceType' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Orchestration/Resource/ResourceType.php', + 'OpenCloud\\Orchestration\\Resource\\Stack' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Orchestration/Resource/Stack.php', 'OpenCloud\\Orchestration\\Service' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Orchestration/Service.php', - 'OpenCloud\\Orchestration\\Stack' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Orchestration/Stack.php', + 'OpenCloud\\Queues\\Collection\\MessageIterator' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Queues/Collection/MessageIterator.php', 'OpenCloud\\Queues\\Exception\\DeleteMessageException' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Queues/Exception/DeleteMessageException.php', 'OpenCloud\\Queues\\Exception\\MessageException' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Queues/Exception/MessageException.php', 'OpenCloud\\Queues\\Exception\\QueueException' => __DIR__ . '/..' . '/rackspace/php-opencloud/lib/OpenCloud/Queues/Exception/QueueException.php', @@ -1894,6 +2286,8 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'aCssRulesetStartToken' => __DIR__ . '/..' . '/natxet/CssMin/src/CssMin.php', 'aCssToken' => __DIR__ . '/..' . '/natxet/CssMin/src/CssMin.php', 'bantu\\IniGetWrapper\\IniGetWrapper' => __DIR__ . '/..' . '/bantu/ini-get-wrapper/src/IniGetWrapper.php', + 'mikemccabe\\JsonPatch\\JsonPatch' => __DIR__ . '/..' . '/mikemccabe/json-patch-php/src/JsonPatch.php', + 'mikemccabe\\JsonPatch\\JsonPatchException' => __DIR__ . '/..' . '/mikemccabe/json-patch-php/src/JsonPatch.php', 'ownCloud\\TarStreamer\\TarStreamer' => __DIR__ . '/..' . '/deepdiver1975/tarstreamer/src/TarStreamer.php', 'phpseclib\\Crypt\\AES' => __DIR__ . '/..' . '/phpseclib/phpseclib/phpseclib/Crypt/AES.php', 'phpseclib\\Crypt\\Base' => __DIR__ . '/..' . '/phpseclib/phpseclib/phpseclib/Crypt/Base.php', diff --git a/composer/installed.json b/composer/installed.json index 8e2207301..e50d84663 100644 --- a/composer/installed.json +++ b/composer/installed.json @@ -1,273 +1,4 @@ [ - { - "name": "guzzle/common", - "version": "v3.8.1", - "version_normalized": "3.8.1.0", - "target-dir": "Guzzle/Common", - "source": { - "type": "git", - "url": "https://github.com/Guzzle3/common.git", - "reference": "67f6c3fd04bae387d47c2a673fa623ed8f4189bb" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Guzzle3/common/zipball/67f6c3fd04bae387d47c2a673fa623ed8f4189bb", - "reference": "67f6c3fd04bae387d47c2a673fa623ed8f4189bb", - "shasum": "" - }, - "require": { - "php": ">=5.3.2", - "symfony/event-dispatcher": ">=2.1" - }, - "time": "2014-01-28T22:29:15+00:00", - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.7-dev" - } - }, - "installation-source": "dist", - "autoload": { - "psr-0": { - "Guzzle\\Common": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Common libraries used by Guzzle", - "homepage": "http://guzzlephp.org/", - "keywords": [ - "collection", - "common", - "event", - "exception" - ], - "abandoned": "guzzle/guzzle" - }, - { - "name": "guzzle/stream", - "version": "v3.8.1", - "version_normalized": "3.8.1.0", - "target-dir": "Guzzle/Stream", - "source": { - "type": "git", - "url": "https://github.com/Guzzle3/stream.git", - "reference": "fa8af730ca714861c0001cfba64aaecc5f21bb96" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Guzzle3/stream/zipball/fa8af730ca714861c0001cfba64aaecc5f21bb96", - "reference": "fa8af730ca714861c0001cfba64aaecc5f21bb96", - "shasum": "" - }, - "require": { - "guzzle/common": "self.version", - "php": ">=5.3.2" - }, - "suggest": { - "guzzle/http": "To convert Guzzle request objects to PHP streams" - }, - "time": "2014-01-28T22:14:17+00:00", - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.7-dev" - } - }, - "installation-source": "dist", - "autoload": { - "psr-0": { - "Guzzle\\Stream": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - } - ], - "description": "Guzzle stream wrapper component", - "homepage": "http://guzzlephp.org/", - "keywords": [ - "Guzzle", - "component", - "stream" - ], - "abandoned": "guzzle/guzzle" - }, - { - "name": "guzzle/parser", - "version": "v3.8.1", - "version_normalized": "3.8.1.0", - "target-dir": "Guzzle/Parser", - "source": { - "type": "git", - "url": "https://github.com/Guzzle3/parser.git", - "reference": "3f52387052f2e4ef083145a0f73c3654aa14e086" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Guzzle3/parser/zipball/3f52387052f2e4ef083145a0f73c3654aa14e086", - "reference": "3f52387052f2e4ef083145a0f73c3654aa14e086", - "shasum": "" - }, - "require": { - "php": ">=5.3.2" - }, - "time": "2013-10-24T00:04:09+00:00", - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.7-dev" - } - }, - "installation-source": "dist", - "autoload": { - "psr-0": { - "Guzzle\\Parser": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Interchangeable parsers used by Guzzle", - "homepage": "http://guzzlephp.org/", - "keywords": [ - "URI Template", - "cookie", - "http", - "message", - "url" - ], - "abandoned": "guzzle/guzzle" - }, - { - "name": "guzzle/http", - "version": "v3.8.1", - "version_normalized": "3.8.1.0", - "target-dir": "Guzzle/Http", - "source": { - "type": "git", - "url": "https://github.com/Guzzle3/http.git", - "reference": "565fd64be16d91c840f497c5de76f86d54a822d8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Guzzle3/http/zipball/565fd64be16d91c840f497c5de76f86d54a822d8", - "reference": "565fd64be16d91c840f497c5de76f86d54a822d8", - "shasum": "" - }, - "require": { - "guzzle/common": "self.version", - "guzzle/parser": "self.version", - "guzzle/stream": "self.version", - "php": ">=5.3.2" - }, - "suggest": { - "ext-curl": "*" - }, - "time": "2014-01-23T18:23:29+00:00", - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.7-dev" - } - }, - "installation-source": "dist", - "autoload": { - "psr-0": { - "Guzzle\\Http": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - } - ], - "description": "HTTP libraries used by Guzzle", - "homepage": "http://guzzlephp.org/", - "keywords": [ - "Guzzle", - "client", - "curl", - "http", - "http client" - ], - "abandoned": "guzzle/guzzle" - }, - { - "name": "rackspace/php-opencloud", - "version": "v1.9.2", - "version_normalized": "1.9.2.0", - "source": { - "type": "git", - "url": "https://github.com/rackspace/php-opencloud.git", - "reference": "6551de7aebcebb369d025662f99ab27c9b4527ac" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/rackspace/php-opencloud/zipball/6551de7aebcebb369d025662f99ab27c9b4527ac", - "reference": "6551de7aebcebb369d025662f99ab27c9b4527ac", - "shasum": "" - }, - "require": { - "guzzle/http": "3.8.*@dev", - "php": ">=5.3.3" - }, - "require-dev": { - "guzzle/guzzle": "dev-master", - "psr/log": "1.0.*", - "satooshi/php-coveralls": "0.6.*@dev" - }, - "time": "2014-02-06T20:53:21+00:00", - "type": "library", - "installation-source": "dist", - "autoload": { - "psr-0": { - "OpenCloud": [ - "lib/", - "tests/" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Glen Campbell", - "email": "glen.campbell@rackspace.com" - }, - { - "name": "Jamie Hannaford", - "email": "jamie.hannaford@rackspace.com", - "homepage": "https://github.com/jamiehannaford" - } - ], - "description": "PHP SDK for Rackspace/OpenStack APIs", - "keywords": [ - "Openstack", - "nova", - "opencloud", - "rackspace", - "swift" - ] - }, { "name": "bantu/ini-get-wrapper", "version": "v1.0.1", @@ -3088,5 +2819,188 @@ "scss", "stylesheet" ] + }, + { + "name": "mikemccabe/json-patch-php", + "version": "0.1.0", + "version_normalized": "0.1.0.0", + "source": { + "type": "git", + "url": "https://github.com/mikemccabe/json-patch-php.git", + "reference": "b3af30a6aec7f6467c773cd49b2d974a70f7c0d4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mikemccabe/json-patch-php/zipball/b3af30a6aec7f6467c773cd49b2d974a70f7c0d4", + "reference": "b3af30a6aec7f6467c773cd49b2d974a70f7c0d4", + "shasum": "" + }, + "time": "2015-01-05T21:19:54+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "mikemccabe\\JsonPatch\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0" + ], + "description": "Produce and apply json-patch objects" + }, + { + "name": "guzzle/guzzle", + "version": "v3.8.1", + "version_normalized": "3.8.1.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "4de0618a01b34aa1c8c33a3f13f396dcd3882eba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/4de0618a01b34aa1c8c33a3f13f396dcd3882eba", + "reference": "4de0618a01b34aa1c8c33a3f13f396dcd3882eba", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "php": ">=5.3.3", + "symfony/event-dispatcher": ">=2.1" + }, + "replace": { + "guzzle/batch": "self.version", + "guzzle/cache": "self.version", + "guzzle/common": "self.version", + "guzzle/http": "self.version", + "guzzle/inflection": "self.version", + "guzzle/iterator": "self.version", + "guzzle/log": "self.version", + "guzzle/parser": "self.version", + "guzzle/plugin": "self.version", + "guzzle/plugin-async": "self.version", + "guzzle/plugin-backoff": "self.version", + "guzzle/plugin-cache": "self.version", + "guzzle/plugin-cookie": "self.version", + "guzzle/plugin-curlauth": "self.version", + "guzzle/plugin-error-response": "self.version", + "guzzle/plugin-history": "self.version", + "guzzle/plugin-log": "self.version", + "guzzle/plugin-md5": "self.version", + "guzzle/plugin-mock": "self.version", + "guzzle/plugin-oauth": "self.version", + "guzzle/service": "self.version", + "guzzle/stream": "self.version" + }, + "require-dev": { + "doctrine/cache": "*", + "monolog/monolog": "1.*", + "phpunit/phpunit": "3.7.*", + "psr/log": "1.0.*", + "symfony/class-loader": "*", + "zendframework/zend-cache": "<2.3", + "zendframework/zend-log": "<2.3" + }, + "time": "2014-01-28T22:29:15+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.8-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-0": { + "Guzzle": "src/", + "Guzzle\\Tests": "tests/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Guzzle Community", + "homepage": "https://github.com/guzzle/guzzle/contributors" + } + ], + "description": "Guzzle is a PHP HTTP client library and framework for building RESTful web service clients", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "rest", + "web service" + ], + "abandoned": "guzzlehttp/guzzle" + }, + { + "name": "rackspace/php-opencloud", + "version": "v1.16.0", + "version_normalized": "1.16.0.0", + "source": { + "type": "git", + "url": "https://github.com/rackspace/php-opencloud.git", + "reference": "d6b71feed7f9e7a4b52e0240a79f06473ba69c8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/rackspace/php-opencloud/zipball/d6b71feed7f9e7a4b52e0240a79f06473ba69c8c", + "reference": "d6b71feed7f9e7a4b52e0240a79f06473ba69c8c", + "shasum": "" + }, + "require": { + "guzzle/guzzle": "~3.8", + "mikemccabe/json-patch-php": "~0.1", + "php": ">=5.4", + "psr/log": "~1.0" + }, + "require-dev": { + "apigen/apigen": "~4.0", + "fabpot/php-cs-fixer": "1.0.*@dev", + "jakub-onderka/php-parallel-lint": "0.*", + "phpspec/prophecy": "~1.4", + "phpunit/phpunit": "4.3.*", + "satooshi/php-coveralls": "0.6.*@dev" + }, + "time": "2016-01-29T10:34:57+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-0": { + "OpenCloud": [ + "lib/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Jamie Hannaford", + "email": "jamie.hannaford@rackspace.com", + "homepage": "https://github.com/jamiehannaford" + } + ], + "description": "PHP SDK for Rackspace/OpenStack APIs", + "keywords": [ + "Openstack", + "nova", + "opencloud", + "rackspace", + "swift" + ] } ] diff --git a/guzzle/guzzle/.gitignore b/guzzle/guzzle/.gitignore new file mode 100644 index 000000000..893035d5b --- /dev/null +++ b/guzzle/guzzle/.gitignore @@ -0,0 +1,27 @@ +# Ingore common cruft +.DS_STORE +coverage +.idea + +# Ignore binary files +guzzle.phar +guzzle-min.phar + +# Ignore potentially sensitive phpunit file +phpunit.xml + +# Ignore composer generated files +composer.phar +composer.lock +composer-test.lock +vendor/ + +# Ignore build files +build/ +phing/build.properties + +# Ignore subsplit working directory +.subsplit + +docs/_build +docs/*.pyc diff --git a/guzzle/guzzle/CHANGELOG.md b/guzzle/guzzle/CHANGELOG.md new file mode 100644 index 000000000..5be7e5dc8 --- /dev/null +++ b/guzzle/guzzle/CHANGELOG.md @@ -0,0 +1,762 @@ +CHANGELOG +========= + +3.8.1 (2013-01-28) +------------------ + +* Bug: Always using GET requests when redirecting from a 303 response +* Bug: CURLOPT_SSL_VERIFYHOST is now correctly set to false when setting `$certificateAuthority` to false in + `Guzzle\Http\ClientInterface::setSslVerification()` +* Bug: RedirectPlugin now uses strict RFC 3986 compliance when combining a base URL with a relative URL +* Bug: The body of a request can now be set to `"0"` +* Sending PHP stream requests no longer forces `HTTP/1.0` +* Adding more information to ExceptionCollection exceptions so that users have more context, including a stack trace of + each sub-exception +* Updated the `$ref` attribute in service descriptions to merge over any existing parameters of a schema (rather than + clobbering everything). +* Merging URLs will now use the query string object from the relative URL (thus allowing custom query aggregators) +* Query strings are now parsed in a way that they do no convert empty keys with no value to have a dangling `=`. + For example `foo&bar=baz` is now correctly parsed and recognized as `foo&bar=baz` rather than `foo=&bar=baz`. +* Now properly escaping the regular expression delimiter when matching Cookie domains. +* Network access is now disabled when loading XML documents + +3.8.0 (2013-12-05) +------------------ + +* Added the ability to define a POST name for a file +* JSON response parsing now properly walks additionalProperties +* cURL error code 18 is now retried automatically in the BackoffPlugin +* Fixed a cURL error when URLs contain fragments +* Fixed an issue in the BackoffPlugin retry event where it was trying to access all exceptions as if they were + CurlExceptions +* CURLOPT_PROGRESS function fix for PHP 5.5 (69fcc1e) +* Added the ability for Guzzle to work with older versions of cURL that do not support `CURLOPT_TIMEOUT_MS` +* Fixed a bug that was encountered when parsing empty header parameters +* UriTemplate now has a `setRegex()` method to match the docs +* The `debug` request parameter now checks if it is truthy rather than if it exists +* Setting the `debug` request parameter to true shows verbose cURL output instead of using the LogPlugin +* Added the ability to combine URLs using strict RFC 3986 compliance +* Command objects can now return the validation errors encountered by the command +* Various fixes to cache revalidation (#437 and 29797e5) +* Various fixes to the AsyncPlugin +* Cleaned up build scripts + +3.7.4 (2013-10-02) +------------------ + +* Bug fix: 0 is now an allowed value in a description parameter that has a default value (#430) +* Bug fix: SchemaFormatter now returns an integer when formatting to a Unix timestamp + (see https://github.com/aws/aws-sdk-php/issues/147) +* Bug fix: Cleaned up and fixed URL dot segment removal to properly resolve internal dots +* Minimum PHP version is now properly specified as 5.3.3 (up from 5.3.2) (#420) +* Updated the bundled cacert.pem (#419) +* OauthPlugin now supports adding authentication to headers or query string (#425) + +3.7.3 (2013-09-08) +------------------ + +* Added the ability to get the exception associated with a request/command when using `MultiTransferException` and + `CommandTransferException`. +* Setting `additionalParameters` of a response to false is now honored when parsing responses with a service description +* Schemas are only injected into response models when explicitly configured. +* No longer guessing Content-Type based on the path of a request. Content-Type is now only guessed based on the path of + an EntityBody. +* Bug fix: ChunkedIterator can now properly chunk a \Traversable as well as an \Iterator. +* Bug fix: FilterIterator now relies on `\Iterator` instead of `\Traversable`. +* Bug fix: Gracefully handling malformed responses in RequestMediator::writeResponseBody() +* Bug fix: Replaced call to canCache with canCacheRequest in the CallbackCanCacheStrategy of the CachePlugin +* Bug fix: Visiting XML attributes first before visting XML children when serializing requests +* Bug fix: Properly parsing headers that contain commas contained in quotes +* Bug fix: mimetype guessing based on a filename is now case-insensitive + +3.7.2 (2013-08-02) +------------------ + +* Bug fix: Properly URL encoding paths when using the PHP-only version of the UriTemplate expander + See https://github.com/guzzle/guzzle/issues/371 +* Bug fix: Cookie domains are now matched correctly according to RFC 6265 + See https://github.com/guzzle/guzzle/issues/377 +* Bug fix: GET parameters are now used when calculating an OAuth signature +* Bug fix: Fixed an issue with cache revalidation where the If-None-Match header was being double quoted +* `Guzzle\Common\AbstractHasDispatcher::dispatch()` now returns the event that was dispatched +* `Guzzle\Http\QueryString::factory()` now guesses the most appropriate query aggregator to used based on the input. + See https://github.com/guzzle/guzzle/issues/379 +* Added a way to add custom domain objects to service description parsing using the `operation.parse_class` event. See + https://github.com/guzzle/guzzle/pull/380 +* cURL multi cleanup and optimizations + +3.7.1 (2013-07-05) +------------------ + +* Bug fix: Setting default options on a client now works +* Bug fix: Setting options on HEAD requests now works. See #352 +* Bug fix: Moving stream factory before send event to before building the stream. See #353 +* Bug fix: Cookies no longer match on IP addresses per RFC 6265 +* Bug fix: Correctly parsing header parameters that are in `<>` and quotes +* Added `cert` and `ssl_key` as request options +* `Host` header can now diverge from the host part of a URL if the header is set manually +* `Guzzle\Service\Command\LocationVisitor\Request\XmlVisitor` was rewritten to change from using SimpleXML to XMLWriter +* OAuth parameters are only added via the plugin if they aren't already set +* Exceptions are now thrown when a URL cannot be parsed +* Returning `false` if `Guzzle\Http\EntityBody::getContentMd5()` fails +* Not setting a `Content-MD5` on a command if calculating the Content-MD5 fails via the CommandContentMd5Plugin + +3.7.0 (2013-06-10) +------------------ + +* See UPGRADING.md for more information on how to upgrade. +* Requests now support the ability to specify an array of $options when creating a request to more easily modify a + request. You can pass a 'request.options' configuration setting to a client to apply default request options to + every request created by a client (e.g. default query string variables, headers, curl options, etc). +* Added a static facade class that allows you to use Guzzle with static methods and mount the class to `\Guzzle`. + See `Guzzle\Http\StaticClient::mount`. +* Added `command.request_options` to `Guzzle\Service\Command\AbstractCommand` to pass request options to requests + created by a command (e.g. custom headers, query string variables, timeout settings, etc). +* Stream size in `Guzzle\Stream\PhpStreamRequestFactory` will now be set if Content-Length is returned in the + headers of a response +* Added `Guzzle\Common\Collection::setPath($path, $value)` to set a value into an array using a nested key + (e.g. `$collection->setPath('foo/baz/bar', 'test'); echo $collection['foo']['bar']['bar'];`) +* ServiceBuilders now support storing and retrieving arbitrary data +* CachePlugin can now purge all resources for a given URI +* CachePlugin can automatically purge matching cached items when a non-idempotent request is sent to a resource +* CachePlugin now uses the Vary header to determine if a resource is a cache hit +* `Guzzle\Http\Message\Response` now implements `\Serializable` +* Added `Guzzle\Cache\CacheAdapterFactory::fromCache()` to more easily create cache adapters +* `Guzzle\Service\ClientInterface::execute()` now accepts an array, single command, or Traversable +* Fixed a bug in `Guzzle\Http\Message\Header\Link::addLink()` +* Better handling of calculating the size of a stream in `Guzzle\Stream\Stream` using fstat() and caching the size +* `Guzzle\Common\Exception\ExceptionCollection` now creates a more readable exception message +* Fixing BC break: Added back the MonologLogAdapter implementation rather than extending from PsrLog so that older + Symfony users can still use the old version of Monolog. +* Fixing BC break: Added the implementation back in for `Guzzle\Http\Message\AbstractMessage::getTokenizedHeader()`. + Now triggering an E_USER_DEPRECATED warning when used. Use `$message->getHeader()->parseParams()`. +* Several performance improvements to `Guzzle\Common\Collection` +* Added an `$options` argument to the end of the following methods of `Guzzle\Http\ClientInterface`: + createRequest, head, delete, put, patch, post, options, prepareRequest +* Added an `$options` argument to the end of `Guzzle\Http\Message\Request\RequestFactoryInterface::createRequest()` +* Added an `applyOptions()` method to `Guzzle\Http\Message\Request\RequestFactoryInterface` +* Changed `Guzzle\Http\ClientInterface::get($uri = null, $headers = null, $body = null)` to + `Guzzle\Http\ClientInterface::get($uri = null, $headers = null, $options = array())`. You can still pass in a + resource, string, or EntityBody into the $options parameter to specify the download location of the response. +* Changed `Guzzle\Common\Collection::__construct($data)` to no longer accepts a null value for `$data` but a + default `array()` +* Added `Guzzle\Stream\StreamInterface::isRepeatable` +* Removed `Guzzle\Http\ClientInterface::setDefaultHeaders(). Use + $client->getConfig()->setPath('request.options/headers/{header_name}', 'value')`. or + $client->getConfig()->setPath('request.options/headers', array('header_name' => 'value'))`. +* Removed `Guzzle\Http\ClientInterface::getDefaultHeaders(). Use $client->getConfig()->getPath('request.options/headers')`. +* Removed `Guzzle\Http\ClientInterface::expandTemplate()` +* Removed `Guzzle\Http\ClientInterface::setRequestFactory()` +* Removed `Guzzle\Http\ClientInterface::getCurlMulti()` +* Removed `Guzzle\Http\Message\RequestInterface::canCache` +* Removed `Guzzle\Http\Message\RequestInterface::setIsRedirect` +* Removed `Guzzle\Http\Message\RequestInterface::isRedirect` +* Made `Guzzle\Http\Client::expandTemplate` and `getUriTemplate` protected methods. +* You can now enable E_USER_DEPRECATED warnings to see if you are using a deprecated method by setting + `Guzzle\Common\Version::$emitWarnings` to true. +* Marked `Guzzle\Http\Message\Request::isResponseBodyRepeatable()` as deprecated. Use + `$request->getResponseBody()->isRepeatable()` instead. +* Marked `Guzzle\Http\Message\Request::canCache()` as deprecated. Use + `Guzzle\Plugin\Cache\DefaultCanCacheStrategy->canCacheRequest()` instead. +* Marked `Guzzle\Http\Message\Request::canCache()` as deprecated. Use + `Guzzle\Plugin\Cache\DefaultCanCacheStrategy->canCacheRequest()` instead. +* Marked `Guzzle\Http\Message\Request::setIsRedirect()` as deprecated. Use the HistoryPlugin instead. +* Marked `Guzzle\Http\Message\Request::isRedirect()` as deprecated. Use the HistoryPlugin instead. +* Marked `Guzzle\Cache\CacheAdapterFactory::factory()` as deprecated +* Marked 'command.headers', 'command.response_body' and 'command.on_complete' as deprecated for AbstractCommand. + These will work through Guzzle 4.0 +* Marked 'request.params' for `Guzzle\Http\Client` as deprecated. Use [request.options][params]. +* Marked `Guzzle\Service\Client::enableMagicMethods()` as deprecated. Magic methods can no longer be disabled on a Guzzle\Service\Client. +* Marked `Guzzle\Service\Client::getDefaultHeaders()` as deprecated. Use $client->getConfig()->getPath('request.options/headers')`. +* Marked `Guzzle\Service\Client::setDefaultHeaders()` as deprecated. Use $client->getConfig()->setPath('request.options/headers/{header_name}', 'value')`. +* Marked `Guzzle\Parser\Url\UrlParser` as deprecated. Just use PHP's `parse_url()` and percent encode your UTF-8. +* Marked `Guzzle\Common\Collection::inject()` as deprecated. +* Marked `Guzzle\Plugin\CurlAuth\CurlAuthPlugin` as deprecated. Use `$client->getConfig()->setPath('request.options/auth', array('user', 'pass', 'Basic|Digest');` +* CacheKeyProviderInterface and DefaultCacheKeyProvider are no longer used. All of this logic is handled in a + CacheStorageInterface. These two objects and interface will be removed in a future version. +* Always setting X-cache headers on cached responses +* Default cache TTLs are now handled by the CacheStorageInterface of a CachePlugin +* `CacheStorageInterface::cache($key, Response $response, $ttl = null)` has changed to `cache(RequestInterface + $request, Response $response);` +* `CacheStorageInterface::fetch($key)` has changed to `fetch(RequestInterface $request);` +* `CacheStorageInterface::delete($key)` has changed to `delete(RequestInterface $request);` +* Added `CacheStorageInterface::purge($url)` +* `DefaultRevalidation::__construct(CacheKeyProviderInterface $cacheKey, CacheStorageInterface $cache, CachePlugin + $plugin)` has changed to `DefaultRevalidation::__construct(CacheStorageInterface $cache, + CanCacheStrategyInterface $canCache = null)` +* Added `RevalidationInterface::shouldRevalidate(RequestInterface $request, Response $response)` + +3.6.0 (2013-05-29) +------------------ + +* ServiceDescription now implements ToArrayInterface +* Added command.hidden_params to blacklist certain headers from being treated as additionalParameters +* Guzzle can now correctly parse incomplete URLs +* Mixed casing of headers are now forced to be a single consistent casing across all values for that header. +* Messages internally use a HeaderCollection object to delegate handling case-insensitive header resolution +* Removed the whole changedHeader() function system of messages because all header changes now go through addHeader(). +* Specific header implementations can be created for complex headers. When a message creates a header, it uses a + HeaderFactory which can map specific headers to specific header classes. There is now a Link header and + CacheControl header implementation. +* Removed from interface: Guzzle\Http\ClientInterface::setUriTemplate +* Removed from interface: Guzzle\Http\ClientInterface::setCurlMulti() +* Removed Guzzle\Http\Message\Request::receivedRequestHeader() and implemented this functionality in + Guzzle\Http\Curl\RequestMediator +* Removed the optional $asString parameter from MessageInterface::getHeader(). Just cast the header to a string. +* Removed the optional $tryChunkedTransfer option from Guzzle\Http\Message\EntityEnclosingRequestInterface +* Removed the $asObjects argument from Guzzle\Http\Message\MessageInterface::getHeaders() +* Removed Guzzle\Parser\ParserRegister::get(). Use getParser() +* Removed Guzzle\Parser\ParserRegister::set(). Use registerParser(). +* All response header helper functions return a string rather than mixing Header objects and strings inconsistently +* Removed cURL blacklist support. This is no longer necessary now that Expect, Accept, etc are managed by Guzzle + directly via interfaces +* Removed the injecting of a request object onto a response object. The methods to get and set a request still exist + but are a no-op until removed. +* Most classes that used to require a ``Guzzle\Service\Command\CommandInterface` typehint now request a + `Guzzle\Service\Command\ArrayCommandInterface`. +* Added `Guzzle\Http\Message\RequestInterface::startResponse()` to the RequestInterface to handle injecting a response + on a request while the request is still being transferred +* The ability to case-insensitively search for header values +* Guzzle\Http\Message\Header::hasExactHeader +* Guzzle\Http\Message\Header::raw. Use getAll() +* Deprecated cache control specific methods on Guzzle\Http\Message\AbstractMessage. Use the CacheControl header object + instead. +* `Guzzle\Service\Command\CommandInterface` now extends from ToArrayInterface and ArrayAccess +* Added the ability to cast Model objects to a string to view debug information. + +3.5.0 (2013-05-13) +------------------ + +* Bug: Fixed a regression so that request responses are parsed only once per oncomplete event rather than multiple times +* Bug: Better cleanup of one-time events accross the board (when an event is meant to fire once, it will now remove + itself from the EventDispatcher) +* Bug: `Guzzle\Log\MessageFormatter` now properly writes "total_time" and "connect_time" values +* Bug: Cloning an EntityEnclosingRequest now clones the EntityBody too +* Bug: Fixed an undefined index error when parsing nested JSON responses with a sentAs parameter that reference a + non-existent key +* Bug: All __call() method arguments are now required (helps with mocking frameworks) +* Deprecating Response::getRequest() and now using a shallow clone of a request object to remove a circular reference + to help with refcount based garbage collection of resources created by sending a request +* Deprecating ZF1 cache and log adapters. These will be removed in the next major version. +* Deprecating `Response::getPreviousResponse()` (method signature still exists, but it'sdeprecated). Use the + HistoryPlugin for a history. +* Added a `responseBody` alias for the `response_body` location +* Refactored internals to no longer rely on Response::getRequest() +* HistoryPlugin can now be cast to a string +* HistoryPlugin now logs transactions rather than requests and responses to more accurately keep track of the requests + and responses that are sent over the wire +* Added `getEffectiveUrl()` and `getRedirectCount()` to Response objects + +3.4.3 (2013-04-30) +------------------ + +* Bug fix: Fixing bug introduced in 3.4.2 where redirect responses are duplicated on the final redirected response +* Added a check to re-extract the temp cacert bundle from the phar before sending each request + +3.4.2 (2013-04-29) +------------------ + +* Bug fix: Stream objects now work correctly with "a" and "a+" modes +* Bug fix: Removing `Transfer-Encoding: chunked` header when a Content-Length is present +* Bug fix: AsyncPlugin no longer forces HEAD requests +* Bug fix: DateTime timezones are now properly handled when using the service description schema formatter +* Bug fix: CachePlugin now properly handles stale-if-error directives when a request to the origin server fails +* Setting a response on a request will write to the custom request body from the response body if one is specified +* LogPlugin now writes to php://output when STDERR is undefined +* Added the ability to set multiple POST files for the same key in a single call +* application/x-www-form-urlencoded POSTs now use the utf-8 charset by default +* Added the ability to queue CurlExceptions to the MockPlugin +* Cleaned up how manual responses are queued on requests (removed "queued_response" and now using request.before_send) +* Configuration loading now allows remote files + +3.4.1 (2013-04-16) +------------------ + +* Large refactoring to how CurlMulti handles work. There is now a proxy that sits in front of a pool of CurlMulti + handles. This greatly simplifies the implementation, fixes a couple bugs, and provides a small performance boost. +* Exceptions are now properly grouped when sending requests in parallel +* Redirects are now properly aggregated when a multi transaction fails +* Redirects now set the response on the original object even in the event of a failure +* Bug fix: Model names are now properly set even when using $refs +* Added support for PHP 5.5's CurlFile to prevent warnings with the deprecated @ syntax +* Added support for oauth_callback in OAuth signatures +* Added support for oauth_verifier in OAuth signatures +* Added support to attempt to retrieve a command first literally, then ucfirst, the with inflection + +3.4.0 (2013-04-11) +------------------ + +* Bug fix: URLs are now resolved correctly based on http://tools.ietf.org/html/rfc3986#section-5.2. #289 +* Bug fix: Absolute URLs with a path in a service description will now properly override the base URL. #289 +* Bug fix: Parsing a query string with a single PHP array value will now result in an array. #263 +* Bug fix: Better normalization of the User-Agent header to prevent duplicate headers. #264. +* Bug fix: Added `number` type to service descriptions. +* Bug fix: empty parameters are removed from an OAuth signature +* Bug fix: Revalidating a cache entry prefers the Last-Modified over the Date header +* Bug fix: Fixed "array to string" error when validating a union of types in a service description +* Bug fix: Removed code that attempted to determine the size of a stream when data is written to the stream +* Bug fix: Not including an `oauth_token` if the value is null in the OauthPlugin. +* Bug fix: Now correctly aggregating successful requests and failed requests in CurlMulti when a redirect occurs. +* The new default CURLOPT_TIMEOUT setting has been increased to 150 seconds so that Guzzle works on poor connections. +* Added a feature to EntityEnclosingRequest::setBody() that will automatically set the Content-Type of the request if + the Content-Type can be determined based on the entity body or the path of the request. +* Added the ability to overwrite configuration settings in a client when grabbing a throwaway client from a builder. +* Added support for a PSR-3 LogAdapter. +* Added a `command.after_prepare` event +* Added `oauth_callback` parameter to the OauthPlugin +* Added the ability to create a custom stream class when using a stream factory +* Added a CachingEntityBody decorator +* Added support for `additionalParameters` in service descriptions to define how custom parameters are serialized. +* The bundled SSL certificate is now provided in the phar file and extracted when running Guzzle from a phar. +* You can now send any EntityEnclosingRequest with POST fields or POST files and cURL will handle creating bodies +* POST requests using a custom entity body are now treated exactly like PUT requests but with a custom cURL method. This + means that the redirect behavior of POST requests with custom bodies will not be the same as POST requests that use + POST fields or files (the latter is only used when emulating a form POST in the browser). +* Lots of cleanup to CurlHandle::factory and RequestFactory::createRequest + +3.3.1 (2013-03-10) +------------------ + +* Added the ability to create PHP streaming responses from HTTP requests +* Bug fix: Running any filters when parsing response headers with service descriptions +* Bug fix: OauthPlugin fixes to allow for multi-dimensional array signing, and sorting parameters before signing +* Bug fix: Removed the adding of default empty arrays and false Booleans to responses in order to be consistent across + response location visitors. +* Bug fix: Removed the possibility of creating configuration files with circular dependencies +* RequestFactory::create() now uses the key of a POST file when setting the POST file name +* Added xmlAllowEmpty to serialize an XML body even if no XML specific parameters are set + +3.3.0 (2013-03-03) +------------------ + +* A large number of performance optimizations have been made +* Bug fix: Added 'wb' as a valid write mode for streams +* Bug fix: `Guzzle\Http\Message\Response::json()` now allows scalar values to be returned +* Bug fix: Fixed bug in `Guzzle\Http\Message\Response` where wrapping quotes were stripped from `getEtag()` +* BC: Removed `Guzzle\Http\Utils` class +* BC: Setting a service description on a client will no longer modify the client's command factories. +* BC: Emitting IO events from a RequestMediator is now a parameter that must be set in a request's curl options using + the 'emit_io' key. This was previously set under a request's parameters using 'curl.emit_io' +* BC: `Guzzle\Stream\Stream::getWrapper()` and `Guzzle\Stream\Stream::getSteamType()` are no longer converted to + lowercase +* Operation parameter objects are now lazy loaded internally +* Added ErrorResponsePlugin that can throw errors for responses defined in service description operations' errorResponses +* Added support for instantiating responseType=class responseClass classes. Classes must implement + `Guzzle\Service\Command\ResponseClassInterface` +* Added support for additionalProperties for top-level parameters in responseType=model responseClasses. These + additional properties also support locations and can be used to parse JSON responses where the outermost part of the + JSON is an array +* Added support for nested renaming of JSON models (rename sentAs to name) +* CachePlugin + * Added support for stale-if-error so that the CachePlugin can now serve stale content from the cache on error + * Debug headers can now added to cached response in the CachePlugin + +3.2.0 (2013-02-14) +------------------ + +* CurlMulti is no longer reused globally. A new multi object is created per-client. This helps to isolate clients. +* URLs with no path no longer contain a "/" by default +* Guzzle\Http\QueryString does no longer manages the leading "?". This is now handled in Guzzle\Http\Url. +* BadResponseException no longer includes the full request and response message +* Adding setData() to Guzzle\Service\Description\ServiceDescriptionInterface +* Adding getResponseBody() to Guzzle\Http\Message\RequestInterface +* Various updates to classes to use ServiceDescriptionInterface type hints rather than ServiceDescription +* Header values can now be normalized into distinct values when multiple headers are combined with a comma separated list +* xmlEncoding can now be customized for the XML declaration of a XML service description operation +* Guzzle\Http\QueryString now uses Guzzle\Http\QueryAggregator\QueryAggregatorInterface objects to add custom value + aggregation and no longer uses callbacks +* The URL encoding implementation of Guzzle\Http\QueryString can now be customized +* Bug fix: Filters were not always invoked for array service description parameters +* Bug fix: Redirects now use a target response body rather than a temporary response body +* Bug fix: The default exponential backoff BackoffPlugin was not giving when the request threshold was exceeded +* Bug fix: Guzzle now takes the first found value when grabbing Cache-Control directives + +3.1.2 (2013-01-27) +------------------ + +* Refactored how operation responses are parsed. Visitors now include a before() method responsible for parsing the + response body. For example, the XmlVisitor now parses the XML response into an array in the before() method. +* Fixed an issue where cURL would not automatically decompress responses when the Accept-Encoding header was sent +* CURLOPT_SSL_VERIFYHOST is never set to 1 because it is deprecated (see 5e0ff2ef20f839e19d1eeb298f90ba3598784444) +* Fixed a bug where redirect responses were not chained correctly using getPreviousResponse() +* Setting default headers on a client after setting the user-agent will not erase the user-agent setting + +3.1.1 (2013-01-20) +------------------ + +* Adding wildcard support to Guzzle\Common\Collection::getPath() +* Adding alias support to ServiceBuilder configs +* Adding Guzzle\Service\Resource\CompositeResourceIteratorFactory and cleaning up factory interface + +3.1.0 (2013-01-12) +------------------ + +* BC: CurlException now extends from RequestException rather than BadResponseException +* BC: Renamed Guzzle\Plugin\Cache\CanCacheStrategyInterface::canCache() to canCacheRequest() and added CanCacheResponse() +* Added getData to ServiceDescriptionInterface +* Added context array to RequestInterface::setState() +* Bug: Removing hard dependency on the BackoffPlugin from Guzzle\Http +* Bug: Adding required content-type when JSON request visitor adds JSON to a command +* Bug: Fixing the serialization of a service description with custom data +* Made it easier to deal with exceptions thrown when transferring commands or requests in parallel by providing + an array of successful and failed responses +* Moved getPath from Guzzle\Service\Resource\Model to Guzzle\Common\Collection +* Added Guzzle\Http\IoEmittingEntityBody +* Moved command filtration from validators to location visitors +* Added `extends` attributes to service description parameters +* Added getModels to ServiceDescriptionInterface + +3.0.7 (2012-12-19) +------------------ + +* Fixing phar detection when forcing a cacert to system if null or true +* Allowing filename to be passed to `Guzzle\Http\Message\Request::setResponseBody()` +* Cleaning up `Guzzle\Common\Collection::inject` method +* Adding a response_body location to service descriptions + +3.0.6 (2012-12-09) +------------------ + +* CurlMulti performance improvements +* Adding setErrorResponses() to Operation +* composer.json tweaks + +3.0.5 (2012-11-18) +------------------ + +* Bug: Fixing an infinite recursion bug caused from revalidating with the CachePlugin +* Bug: Response body can now be a string containing "0" +* Bug: Using Guzzle inside of a phar uses system by default but now allows for a custom cacert +* Bug: QueryString::fromString now properly parses query string parameters that contain equal signs +* Added support for XML attributes in service description responses +* DefaultRequestSerializer now supports array URI parameter values for URI template expansion +* Added better mimetype guessing to requests and post files + +3.0.4 (2012-11-11) +------------------ + +* Bug: Fixed a bug when adding multiple cookies to a request to use the correct glue value +* Bug: Cookies can now be added that have a name, domain, or value set to "0" +* Bug: Using the system cacert bundle when using the Phar +* Added json and xml methods to Response to make it easier to parse JSON and XML response data into data structures +* Enhanced cookie jar de-duplication +* Added the ability to enable strict cookie jars that throw exceptions when invalid cookies are added +* Added setStream to StreamInterface to actually make it possible to implement custom rewind behavior for entity bodies +* Added the ability to create any sort of hash for a stream rather than just an MD5 hash + +3.0.3 (2012-11-04) +------------------ + +* Implementing redirects in PHP rather than cURL +* Added PECL URI template extension and using as default parser if available +* Bug: Fixed Content-Length parsing of Response factory +* Adding rewind() method to entity bodies and streams. Allows for custom rewinding of non-repeatable streams. +* Adding ToArrayInterface throughout library +* Fixing OauthPlugin to create unique nonce values per request + +3.0.2 (2012-10-25) +------------------ + +* Magic methods are enabled by default on clients +* Magic methods return the result of a command +* Service clients no longer require a base_url option in the factory +* Bug: Fixed an issue with URI templates where null template variables were being expanded + +3.0.1 (2012-10-22) +------------------ + +* Models can now be used like regular collection objects by calling filter, map, etc +* Models no longer require a Parameter structure or initial data in the constructor +* Added a custom AppendIterator to get around a PHP bug with the `\AppendIterator` + +3.0.0 (2012-10-15) +------------------ + +* Rewrote service description format to be based on Swagger + * Now based on JSON schema + * Added nested input structures and nested response models + * Support for JSON and XML input and output models + * Renamed `commands` to `operations` + * Removed dot class notation + * Removed custom types +* Broke the project into smaller top-level namespaces to be more component friendly +* Removed support for XML configs and descriptions. Use arrays or JSON files. +* Removed the Validation component and Inspector +* Moved all cookie code to Guzzle\Plugin\Cookie +* Magic methods on a Guzzle\Service\Client now return the command un-executed. +* Calling getResult() or getResponse() on a command will lazily execute the command if needed. +* Now shipping with cURL's CA certs and using it by default +* Added previousResponse() method to response objects +* No longer sending Accept and Accept-Encoding headers on every request +* Only sending an Expect header by default when a payload is greater than 1MB +* Added/moved client options: + * curl.blacklist to curl.option.blacklist + * Added ssl.certificate_authority +* Added a Guzzle\Iterator component +* Moved plugins from Guzzle\Http\Plugin to Guzzle\Plugin +* Added a more robust backoff retry strategy (replaced the ExponentialBackoffPlugin) +* Added a more robust caching plugin +* Added setBody to response objects +* Updating LogPlugin to use a more flexible MessageFormatter +* Added a completely revamped build process +* Cleaning up Collection class and removing default values from the get method +* Fixed ZF2 cache adapters + +2.8.8 (2012-10-15) +------------------ + +* Bug: Fixed a cookie issue that caused dot prefixed domains to not match where popular browsers did + +2.8.7 (2012-09-30) +------------------ + +* Bug: Fixed config file aliases for JSON includes +* Bug: Fixed cookie bug on a request object by using CookieParser to parse cookies on requests +* Bug: Removing the path to a file when sending a Content-Disposition header on a POST upload +* Bug: Hardening request and response parsing to account for missing parts +* Bug: Fixed PEAR packaging +* Bug: Fixed Request::getInfo +* Bug: Fixed cases where CURLM_CALL_MULTI_PERFORM return codes were causing curl transactions to fail +* Adding the ability for the namespace Iterator factory to look in multiple directories +* Added more getters/setters/removers from service descriptions +* Added the ability to remove POST fields from OAuth signatures +* OAuth plugin now supports 2-legged OAuth + +2.8.6 (2012-09-05) +------------------ + +* Added the ability to modify and build service descriptions +* Added the use of visitors to apply parameters to locations in service descriptions using the dynamic command +* Added a `json` parameter location +* Now allowing dot notation for classes in the CacheAdapterFactory +* Using the union of two arrays rather than an array_merge when extending service builder services and service params +* Ensuring that a service is a string before doing strpos() checks on it when substituting services for references + in service builder config files. +* Services defined in two different config files that include one another will by default replace the previously + defined service, but you can now create services that extend themselves and merge their settings over the previous +* The JsonLoader now supports aliasing filenames with different filenames. This allows you to alias something like + '_default' with a default JSON configuration file. + +2.8.5 (2012-08-29) +------------------ + +* Bug: Suppressed empty arrays from URI templates +* Bug: Added the missing $options argument from ServiceDescription::factory to enable caching +* Added support for HTTP responses that do not contain a reason phrase in the start-line +* AbstractCommand commands are now invokable +* Added a way to get the data used when signing an Oauth request before a request is sent + +2.8.4 (2012-08-15) +------------------ + +* Bug: Custom delay time calculations are no longer ignored in the ExponentialBackoffPlugin +* Added the ability to transfer entity bodies as a string rather than streamed. This gets around curl error 65. Set `body_as_string` in a request's curl options to enable. +* Added a StreamInterface, EntityBodyInterface, and added ftell() to Guzzle\Common\Stream +* Added an AbstractEntityBodyDecorator and a ReadLimitEntityBody decorator to transfer only a subset of a decorated stream +* Stream and EntityBody objects will now return the file position to the previous position after a read required operation (e.g. getContentMd5()) +* Added additional response status codes +* Removed SSL information from the default User-Agent header +* DELETE requests can now send an entity body +* Added an EventDispatcher to the ExponentialBackoffPlugin and added an ExponentialBackoffLogger to log backoff retries +* Added the ability of the MockPlugin to consume mocked request bodies +* LogPlugin now exposes request and response objects in the extras array + +2.8.3 (2012-07-30) +------------------ + +* Bug: Fixed a case where empty POST requests were sent as GET requests +* Bug: Fixed a bug in ExponentialBackoffPlugin that caused fatal errors when retrying an EntityEnclosingRequest that does not have a body +* Bug: Setting the response body of a request to null after completing a request, not when setting the state of a request to new +* Added multiple inheritance to service description commands +* Added an ApiCommandInterface and added ``getParamNames()`` and ``hasParam()`` +* Removed the default 2mb size cutoff from the Md5ValidatorPlugin so that it now defaults to validating everything +* Changed CurlMulti::perform to pass a smaller timeout to CurlMulti::executeHandles + +2.8.2 (2012-07-24) +------------------ + +* Bug: Query string values set to 0 are no longer dropped from the query string +* Bug: A Collection object is no longer created each time a call is made to ``Guzzle\Service\Command\AbstractCommand::getRequestHeaders()`` +* Bug: ``+`` is now treated as an encoded space when parsing query strings +* QueryString and Collection performance improvements +* Allowing dot notation for class paths in filters attribute of a service descriptions + +2.8.1 (2012-07-16) +------------------ + +* Loosening Event Dispatcher dependency +* POST redirects can now be customized using CURLOPT_POSTREDIR + +2.8.0 (2012-07-15) +------------------ + +* BC: Guzzle\Http\Query + * Query strings with empty variables will always show an equal sign unless the variable is set to QueryString::BLANK (e.g. ?acl= vs ?acl) + * Changed isEncodingValues() and isEncodingFields() to isUrlEncoding() + * Changed setEncodeValues(bool) and setEncodeFields(bool) to useUrlEncoding(bool) + * Changed the aggregation functions of QueryString to be static methods + * Can now use fromString() with querystrings that have a leading ? +* cURL configuration values can be specified in service descriptions using ``curl.`` prefixed parameters +* Content-Length is set to 0 before emitting the request.before_send event when sending an empty request body +* Cookies are no longer URL decoded by default +* Bug: URI template variables set to null are no longer expanded + +2.7.2 (2012-07-02) +------------------ + +* BC: Moving things to get ready for subtree splits. Moving Inflection into Common. Moving Guzzle\Http\Parser to Guzzle\Parser. +* BC: Removing Guzzle\Common\Batch\Batch::count() and replacing it with isEmpty() +* CachePlugin now allows for a custom request parameter function to check if a request can be cached +* Bug fix: CachePlugin now only caches GET and HEAD requests by default +* Bug fix: Using header glue when transferring headers over the wire +* Allowing deeply nested arrays for composite variables in URI templates +* Batch divisors can now return iterators or arrays + +2.7.1 (2012-06-26) +------------------ + +* Minor patch to update version number in UA string +* Updating build process + +2.7.0 (2012-06-25) +------------------ + +* BC: Inflection classes moved to Guzzle\Inflection. No longer static methods. Can now inject custom inflectors into classes. +* BC: Removed magic setX methods from commands +* BC: Magic methods mapped to service description commands are now inflected in the command factory rather than the client __call() method +* Verbose cURL options are no longer enabled by default. Set curl.debug to true on a client to enable. +* Bug: Now allowing colons in a response start-line (e.g. HTTP/1.1 503 Service Unavailable: Back-end server is at capacity) +* Guzzle\Service\Resource\ResourceIteratorApplyBatched now internally uses the Guzzle\Common\Batch namespace +* Added Guzzle\Service\Plugin namespace and a PluginCollectionPlugin +* Added the ability to set POST fields and files in a service description +* Guzzle\Http\EntityBody::factory() now accepts objects with a __toString() method +* Adding a command.before_prepare event to clients +* Added BatchClosureTransfer and BatchClosureDivisor +* BatchTransferException now includes references to the batch divisor and transfer strategies +* Fixed some tests so that they pass more reliably +* Added Guzzle\Common\Log\ArrayLogAdapter + +2.6.6 (2012-06-10) +------------------ + +* BC: Removing Guzzle\Http\Plugin\BatchQueuePlugin +* BC: Removing Guzzle\Service\Command\CommandSet +* Adding generic batching system (replaces the batch queue plugin and command set) +* Updating ZF cache and log adapters and now using ZF's composer repository +* Bug: Setting the name of each ApiParam when creating through an ApiCommand +* Adding result_type, result_doc, deprecated, and doc_url to service descriptions +* Bug: Changed the default cookie header casing back to 'Cookie' + +2.6.5 (2012-06-03) +------------------ + +* BC: Renaming Guzzle\Http\Message\RequestInterface::getResourceUri() to getResource() +* BC: Removing unused AUTH_BASIC and AUTH_DIGEST constants from +* BC: Guzzle\Http\Cookie is now used to manage Set-Cookie data, not Cookie data +* BC: Renaming methods in the CookieJarInterface +* Moving almost all cookie logic out of the CookiePlugin and into the Cookie or CookieJar implementations +* Making the default glue for HTTP headers ';' instead of ',' +* Adding a removeValue to Guzzle\Http\Message\Header +* Adding getCookies() to request interface. +* Making it easier to add event subscribers to HasDispatcherInterface classes. Can now directly call addSubscriber() + +2.6.4 (2012-05-30) +------------------ + +* BC: Cleaning up how POST files are stored in EntityEnclosingRequest objects. Adding PostFile class. +* BC: Moving ApiCommand specific functionality from the Inspector and on to the ApiCommand +* Bug: Fixing magic method command calls on clients +* Bug: Email constraint only validates strings +* Bug: Aggregate POST fields when POST files are present in curl handle +* Bug: Fixing default User-Agent header +* Bug: Only appending or prepending parameters in commands if they are specified +* Bug: Not requiring response reason phrases or status codes to match a predefined list of codes +* Allowing the use of dot notation for class namespaces when using instance_of constraint +* Added any_match validation constraint +* Added an AsyncPlugin +* Passing request object to the calculateWait method of the ExponentialBackoffPlugin +* Allowing the result of a command object to be changed +* Parsing location and type sub values when instantiating a service description rather than over and over at runtime + +2.6.3 (2012-05-23) +------------------ + +* [BC] Guzzle\Common\FromConfigInterface no longer requires any config options. +* [BC] Refactoring how POST files are stored on an EntityEnclosingRequest. They are now separate from POST fields. +* You can now use an array of data when creating PUT request bodies in the request factory. +* Removing the requirement that HTTPS requests needed a Cache-Control: public directive to be cacheable. +* [Http] Adding support for Content-Type in multipart POST uploads per upload +* [Http] Added support for uploading multiple files using the same name (foo[0], foo[1]) +* Adding more POST data operations for easier manipulation of POST data. +* You can now set empty POST fields. +* The body of a request is only shown on EntityEnclosingRequest objects that do not use POST files. +* Split the Guzzle\Service\Inspector::validateConfig method into two methods. One to initialize when a command is created, and one to validate. +* CS updates + +2.6.2 (2012-05-19) +------------------ + +* [Http] Better handling of nested scope requests in CurlMulti. Requests are now always prepares in the send() method rather than the addRequest() method. + +2.6.1 (2012-05-19) +------------------ + +* [BC] Removing 'path' support in service descriptions. Use 'uri'. +* [BC] Guzzle\Service\Inspector::parseDocBlock is now protected. Adding getApiParamsForClass() with cache. +* [BC] Removing Guzzle\Common\NullObject. Use https://github.com/mtdowling/NullObject if you need it. +* [BC] Removing Guzzle\Common\XmlElement. +* All commands, both dynamic and concrete, have ApiCommand objects. +* Adding a fix for CurlMulti so that if all of the connections encounter some sort of curl error, then the loop exits. +* Adding checks to EntityEnclosingRequest so that empty POST files and fields are ignored. +* Making the method signature of Guzzle\Service\Builder\ServiceBuilder::factory more flexible. + +2.6.0 (2012-05-15) +------------------ + +* [BC] Moving Guzzle\Service\Builder to Guzzle\Service\Builder\ServiceBuilder +* [BC] Executing a Command returns the result of the command rather than the command +* [BC] Moving all HTTP parsing logic to Guzzle\Http\Parsers. Allows for faster C implementations if needed. +* [BC] Changing the Guzzle\Http\Message\Response::setProtocol() method to accept a protocol and version in separate args. +* [BC] Moving ResourceIterator* to Guzzle\Service\Resource +* [BC] Completely refactored ResourceIterators to iterate over a cloned command object +* [BC] Moved Guzzle\Http\UriTemplate to Guzzle\Http\Parser\UriTemplate\UriTemplate +* [BC] Guzzle\Guzzle is now deprecated +* Moving Guzzle\Common\Guzzle::inject to Guzzle\Common\Collection::inject +* Adding Guzzle\Version class to give version information about Guzzle +* Adding Guzzle\Http\Utils class to provide getDefaultUserAgent() and getHttpDate() +* Adding Guzzle\Curl\CurlVersion to manage caching curl_version() data +* ServiceDescription and ServiceBuilder are now cacheable using similar configs +* Changing the format of XML and JSON service builder configs. Backwards compatible. +* Cleaned up Cookie parsing +* Trimming the default Guzzle User-Agent header +* Adding a setOnComplete() method to Commands that is called when a command completes +* Keeping track of requests that were mocked in the MockPlugin +* Fixed a caching bug in the CacheAdapterFactory +* Inspector objects can be injected into a Command object +* Refactoring a lot of code and tests to be case insensitive when dealing with headers +* Adding Guzzle\Http\Message\HeaderComparison for easy comparison of HTTP headers using a DSL +* Adding the ability to set global option overrides to service builder configs +* Adding the ability to include other service builder config files from within XML and JSON files +* Moving the parseQuery method out of Url and on to QueryString::fromString() as a static factory method. + +2.5.0 (2012-05-08) +------------------ + +* Major performance improvements +* [BC] Simplifying Guzzle\Common\Collection. Please check to see if you are using features that are now deprecated. +* [BC] Using a custom validation system that allows a flyweight implementation for much faster validation. No longer using Symfony2 Validation component. +* [BC] No longer supporting "{{ }}" for injecting into command or UriTemplates. Use "{}" +* Added the ability to passed parameters to all requests created by a client +* Added callback functionality to the ExponentialBackoffPlugin +* Using microtime in ExponentialBackoffPlugin to allow more granular backoff strategies. +* Rewinding request stream bodies when retrying requests +* Exception is thrown when JSON response body cannot be decoded +* Added configurable magic method calls to clients and commands. This is off by default. +* Fixed a defect that added a hash to every parsed URL part +* Fixed duplicate none generation for OauthPlugin. +* Emitting an event each time a client is generated by a ServiceBuilder +* Using an ApiParams object instead of a Collection for parameters of an ApiCommand +* cache.* request parameters should be renamed to params.cache.* +* Added the ability to set arbitrary curl options on requests (disable_wire, progress, etc). See CurlHandle. +* Added the ability to disable type validation of service descriptions +* ServiceDescriptions and ServiceBuilders are now Serializable diff --git a/guzzle/guzzle/LICENSE b/guzzle/guzzle/LICENSE new file mode 100644 index 000000000..d51aa6986 --- /dev/null +++ b/guzzle/guzzle/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2011 Michael Dowling, https://github.com/mtdowling + +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. diff --git a/guzzle/guzzle/README.md b/guzzle/guzzle/README.md new file mode 100644 index 000000000..108ba5286 --- /dev/null +++ b/guzzle/guzzle/README.md @@ -0,0 +1,212 @@ +Guzzle, PHP HTTP client and webservice framework +================================================ + +[![Latest Stable Version](https://poser.pugx.org/guzzle/guzzle/version.png)](https://packagist.org/packages/guzzle/guzzle) [![Composer Downloads](https://poser.pugx.org/guzzle/guzzle/d/total.png)](https://packagist.org/packages/guzzle/guzzle) [![Build Status](https://secure.travis-ci.org/guzzle/guzzle.png?branch=master)](http://travis-ci.org/guzzle/guzzle) + +Guzzle is a PHP HTTP client and framework for building RESTful web service clients. + +- Extremely powerful API provides all the power of cURL with a simple interface. +- Truly take advantage of HTTP/1.1 with persistent connections, connection pooling, and parallel requests. +- Service description DSL allows you build awesome web service clients faster. +- Symfony2 event-based plugin system allows you to completely modify the behavior of a request. + +Get answers with: [Documentation](http://docs.guzzlephp.org/), [Forums](https://groups.google.com/forum/?hl=en#!forum/guzzle), IRC ([#guzzlephp](irc://irc.freenode.net/#guzzlephp) @ irc.freenode.net) + +```php +// Really simple using a static facade +Guzzle\Http\StaticClient::mount(); +$response = Guzzle::get('http://guzzlephp.org'); + +// More control using a client class +$client = new \Guzzle\Http\Client('http://guzzlephp.org'); +$request = $client->get('/'); +$response = $request->send(); +``` + +### Installing via Composer + +The recommended way to install Guzzle is through [Composer](http://getcomposer.org). + +```bash +# Install Composer +curl -sS https://getcomposer.org/installer | php + +# Add Guzzle as a dependency +php composer.phar require guzzle/guzzle:~3.7 +``` + +After installing, you need to require Composer's autoloader: + +```php +require 'vendor/autoload.php'; +``` + +### Installing via phar + +As of the 3.7.4 release, each release of Guzzle includes a "guzzle.phar" file that includes all of the files needed to +run Guzzle and all of Guzzle's dependencies. Simply download the phar and include it in your project. + +You can find a list of each release and the available downloads at https://github.com/guzzle/guzzle/releases. + +Features +-------- + +- Supports GET, HEAD, POST, DELETE, PUT, PATCH, OPTIONS, and any other custom HTTP method +- Allows full access to request and response headers +- Persistent connections are implicitly managed by Guzzle, resulting in huge performance benefits +- [Send requests in parallel](http://guzzlephp.org/tour/http.html#send-http-requests-in-parallel) +- Cookie sessions can be maintained between requests using the + [CookiePlugin](http://guzzlephp.org/tour/http.html#cookie-session-plugin) +- Allows custom [entity bodies](http://guzzlephp.org/tour/http.html#entity-bodies), including sending data from a PHP + stream and downloading data to a PHP stream +- Responses can be cached and served from cache using the + [caching forward proxy plugin](http://guzzlephp.org/tour/http.html#php-based-caching-forward-proxy) +- Failed requests can be retried using + [truncated exponential backoff](http://guzzlephp.org/tour/http.html#truncated-exponential-backoff) with custom retry + policies +- Entity bodies can be validated automatically using Content-MD5 headers and the + [MD5 hash validator plugin](http://guzzlephp.org/tour/http.html#md5-hash-validator-plugin) +- All data sent over the wire can be logged using the + [LogPlugin](http://guzzlephp.org/tour/http.html#over-the-wire-logging) +- Subject/Observer signal slot system for unobtrusively + [modifying request behavior](http://guzzlephp.org/guide/http/creating_plugins.html) +- Supports all of the features of libcurl including authentication, compression, redirects, SSL, proxies, etc +- Web service client framework for building future-proof interfaces to web services +- Includes a [service description DSL](http://guzzlephp.org/guide/service/service_descriptions.html) for quickly + building webservice clients +- Full support for [URI templates](http://tools.ietf.org/html/rfc6570) +- Advanced batching functionality to efficiently send requests or commands in parallel with customizable batch sizes + and transfer strategies + +HTTP basics +----------- + +```php + '***' +]); + +// Issue a path using a relative URL to the client's base URL +// Sends to http://www.example.com/api/v1/key/***/users +$request = $client->get('users'); +$response = $request->send(); + +// Relative URL that overwrites the path of the base URL +$request = $client->get('/test/123.php?a=b'); + +// Issue a head request on the base URL +$response = $client->head()->send(); +// Delete user 123 +$response = $client->delete('users/123')->send(); + +// Send a PUT request with custom headers +$response = $client->put('upload/text', [ + 'X-Header' => 'My Header' +], 'body of the request')->send(); + +// Send a PUT request using the contents of a PHP stream as the body +// Send using an absolute URL (overrides the base URL) +$response = $client->put('http://www.example.com/upload', [ + 'X-Header' => 'My Header' +], fopen('http://www.test.com/', 'r')); + +// Create a POST request with a file upload (notice the @ symbol): +$request = $client->post('http://localhost:8983/solr/update', null, [ + 'custom_field' => 'my value', + 'file' => '@/path/to/documents.xml' +]); + +// Create a POST request and add the POST files manually +$request = $client->post('http://localhost:8983/solr/update') + ->addPostFiles(['file' => '/path/to/documents.xml']); + +// Responses are objects +echo $response->getStatusCode() . ' ' . $response->getReasonPhrase() . "\n"; + +// Requests and responses can be cast to a string to show the raw HTTP message +echo $request . "\n\n" . $response; + +// Create a request based on an HTTP message +$request = RequestFactory::fromMessage( + "PUT / HTTP/1.1\r\n" . + "Host: test.com:8081\r\n" . + "Content-Type: text/plain" . + "Transfer-Encoding: chunked\r\n" . + "\r\n" . + "this is the body" +); +``` + +Using the static client facade +------------------------------ + +You can use Guzzle through a static client to make it even easier to send simple HTTP requests. + +```php + ['X-Foo' => 'Bar'], + 'body' => ['Foo' => 'Bar'], + 'query' => ['Test' => 123], + 'timeout' => 10, + 'debug' => true, + 'save_to' => '/path/to/file.html' +]); +``` + +### Available request options: + +* headers: Associative array of headers +* query: Associative array of query string values to add to the request +* body: Body of a request, including an EntityBody, string, or array when sending POST requests. Setting a body for a + GET request will set where the response body is downloaded. +* auth: Array of HTTP authentication parameters to use with the request. The array must contain the + username in index [0], the password in index [1], and can optionally contain the authentication type in index [2]. + The authentication types are: "Basic", "Digest". The default auth type is "Basic". +* cookies: Associative array of cookies +* allow_redirects: Set to false to disable redirects +* save_to: String, fopen resource, or EntityBody object used to store the body of the response +* events: Associative array mapping event names to a closure or array of (priority, closure) +* plugins: Array of plugins to add to the request +* exceptions: Set to false to disable throwing exceptions on an HTTP level error (e.g. 404, 500, etc) +* timeout: Float describing the timeout of the request in seconds +* connect_timeout: Float describing the number of seconds to wait while trying to connect. Use 0 to wait + indefinitely. +* verify: Set to true to enable SSL cert validation (the default), false to disable, or supply the path to a CA + bundle to enable verification using a custom certificate. +* proxy: Specify an HTTP proxy (e.g. "http://username:password@192.168.16.1:10") +* debug: Set to true to display all data sent over the wire + +These options can also be used when creating requests using a standard client: + +```php +$client = new Guzzle\Http\Client(); +// Create a request with a timeout of 10 seconds +$request = $client->get('http://guzzlephp.org', [], ['timeout' => 10]); +$response = $request->send(); +``` + +Unit testing +------------ + +Guzzle uses PHPUnit for unit testing. In order to run the unit tests, you'll first need +to install the dependencies of the project using Composer: `php composer.phar install --dev`. +You can then run the tests using `vendor/bin/phpunit`. + +If you are running the tests with xdebug enabled, you may encounter the following issue: 'Fatal error: Maximum function nesting level of '100' reached, aborting!'. This can be resolved by adding 'xdebug.max_nesting_level = 200' to your php.ini file. + +The PECL extensions, uri_template and pecl_http will be required to ensure all the tests can run. diff --git a/guzzle/guzzle/UPGRADING.md b/guzzle/guzzle/UPGRADING.md new file mode 100644 index 000000000..f58bf1171 --- /dev/null +++ b/guzzle/guzzle/UPGRADING.md @@ -0,0 +1,537 @@ +Guzzle Upgrade Guide +==================== + +3.6 to 3.7 +---------- + +### Deprecations + +- You can now enable E_USER_DEPRECATED warnings to see if you are using any deprecated methods.: + +```php +\Guzzle\Common\Version::$emitWarnings = true; +``` + +The following APIs and options have been marked as deprecated: + +- Marked `Guzzle\Http\Message\Request::isResponseBodyRepeatable()` as deprecated. Use `$request->getResponseBody()->isRepeatable()` instead. +- Marked `Guzzle\Http\Message\Request::canCache()` as deprecated. Use `Guzzle\Plugin\Cache\DefaultCanCacheStrategy->canCacheRequest()` instead. +- Marked `Guzzle\Http\Message\Request::canCache()` as deprecated. Use `Guzzle\Plugin\Cache\DefaultCanCacheStrategy->canCacheRequest()` instead. +- Marked `Guzzle\Http\Message\Request::setIsRedirect()` as deprecated. Use the HistoryPlugin instead. +- Marked `Guzzle\Http\Message\Request::isRedirect()` as deprecated. Use the HistoryPlugin instead. +- Marked `Guzzle\Cache\CacheAdapterFactory::factory()` as deprecated +- Marked `Guzzle\Service\Client::enableMagicMethods()` as deprecated. Magic methods can no longer be disabled on a Guzzle\Service\Client. +- Marked `Guzzle\Parser\Url\UrlParser` as deprecated. Just use PHP's `parse_url()` and percent encode your UTF-8. +- Marked `Guzzle\Common\Collection::inject()` as deprecated. +- Marked `Guzzle\Plugin\CurlAuth\CurlAuthPlugin` as deprecated. Use + `$client->getConfig()->setPath('request.options/auth', array('user', 'pass', 'Basic|Digest|NTLM|Any'));` or + `$client->setDefaultOption('auth', array('user', 'pass', 'Basic|Digest|NTLM|Any'));` + +3.7 introduces `request.options` as a parameter for a client configuration and as an optional argument to all creational +request methods. When paired with a client's configuration settings, these options allow you to specify default settings +for various aspects of a request. Because these options make other previous configuration options redundant, several +configuration options and methods of a client and AbstractCommand have been deprecated. + +- Marked `Guzzle\Service\Client::getDefaultHeaders()` as deprecated. Use `$client->getDefaultOption('headers')`. +- Marked `Guzzle\Service\Client::setDefaultHeaders()` as deprecated. Use `$client->setDefaultOption('headers/{header_name}', 'value')`. +- Marked 'request.params' for `Guzzle\Http\Client` as deprecated. Use `$client->setDefaultOption('params/{param_name}', 'value')` +- Marked 'command.headers', 'command.response_body' and 'command.on_complete' as deprecated for AbstractCommand. These will work through Guzzle 4.0 + + $command = $client->getCommand('foo', array( + 'command.headers' => array('Test' => '123'), + 'command.response_body' => '/path/to/file' + )); + + // Should be changed to: + + $command = $client->getCommand('foo', array( + 'command.request_options' => array( + 'headers' => array('Test' => '123'), + 'save_as' => '/path/to/file' + ) + )); + +### Interface changes + +Additions and changes (you will need to update any implementations or subclasses you may have created): + +- Added an `$options` argument to the end of the following methods of `Guzzle\Http\ClientInterface`: + createRequest, head, delete, put, patch, post, options, prepareRequest +- Added an `$options` argument to the end of `Guzzle\Http\Message\Request\RequestFactoryInterface::createRequest()` +- Added an `applyOptions()` method to `Guzzle\Http\Message\Request\RequestFactoryInterface` +- Changed `Guzzle\Http\ClientInterface::get($uri = null, $headers = null, $body = null)` to + `Guzzle\Http\ClientInterface::get($uri = null, $headers = null, $options = array())`. You can still pass in a + resource, string, or EntityBody into the $options parameter to specify the download location of the response. +- Changed `Guzzle\Common\Collection::__construct($data)` to no longer accepts a null value for `$data` but a + default `array()` +- Added `Guzzle\Stream\StreamInterface::isRepeatable` +- Made `Guzzle\Http\Client::expandTemplate` and `getUriTemplate` protected methods. + +The following methods were removed from interfaces. All of these methods are still available in the concrete classes +that implement them, but you should update your code to use alternative methods: + +- Removed `Guzzle\Http\ClientInterface::setDefaultHeaders(). Use + `$client->getConfig()->setPath('request.options/headers/{header_name}', 'value')`. or + `$client->getConfig()->setPath('request.options/headers', array('header_name' => 'value'))` or + `$client->setDefaultOption('headers/{header_name}', 'value')`. or + `$client->setDefaultOption('headers', array('header_name' => 'value'))`. +- Removed `Guzzle\Http\ClientInterface::getDefaultHeaders(). Use `$client->getConfig()->getPath('request.options/headers')`. +- Removed `Guzzle\Http\ClientInterface::expandTemplate()`. This is an implementation detail. +- Removed `Guzzle\Http\ClientInterface::setRequestFactory()`. This is an implementation detail. +- Removed `Guzzle\Http\ClientInterface::getCurlMulti()`. This is a very specific implementation detail. +- Removed `Guzzle\Http\Message\RequestInterface::canCache`. Use the CachePlugin. +- Removed `Guzzle\Http\Message\RequestInterface::setIsRedirect`. Use the HistoryPlugin. +- Removed `Guzzle\Http\Message\RequestInterface::isRedirect`. Use the HistoryPlugin. + +### Cache plugin breaking changes + +- CacheKeyProviderInterface and DefaultCacheKeyProvider are no longer used. All of this logic is handled in a + CacheStorageInterface. These two objects and interface will be removed in a future version. +- Always setting X-cache headers on cached responses +- Default cache TTLs are now handled by the CacheStorageInterface of a CachePlugin +- `CacheStorageInterface::cache($key, Response $response, $ttl = null)` has changed to `cache(RequestInterface + $request, Response $response);` +- `CacheStorageInterface::fetch($key)` has changed to `fetch(RequestInterface $request);` +- `CacheStorageInterface::delete($key)` has changed to `delete(RequestInterface $request);` +- Added `CacheStorageInterface::purge($url)` +- `DefaultRevalidation::__construct(CacheKeyProviderInterface $cacheKey, CacheStorageInterface $cache, CachePlugin + $plugin)` has changed to `DefaultRevalidation::__construct(CacheStorageInterface $cache, + CanCacheStrategyInterface $canCache = null)` +- Added `RevalidationInterface::shouldRevalidate(RequestInterface $request, Response $response)` + +3.5 to 3.6 +---------- + +* Mixed casing of headers are now forced to be a single consistent casing across all values for that header. +* Messages internally use a HeaderCollection object to delegate handling case-insensitive header resolution +* Removed the whole changedHeader() function system of messages because all header changes now go through addHeader(). + For example, setHeader() first removes the header using unset on a HeaderCollection and then calls addHeader(). + Keeping the Host header and URL host in sync is now handled by overriding the addHeader method in Request. +* Specific header implementations can be created for complex headers. When a message creates a header, it uses a + HeaderFactory which can map specific headers to specific header classes. There is now a Link header and + CacheControl header implementation. +* Moved getLinks() from Response to just be used on a Link header object. + +If you previously relied on Guzzle\Http\Message\Header::raw(), then you will need to update your code to use the +HeaderInterface (e.g. toArray(), getAll(), etc). + +### Interface changes + +* Removed from interface: Guzzle\Http\ClientInterface::setUriTemplate +* Removed from interface: Guzzle\Http\ClientInterface::setCurlMulti() +* Removed Guzzle\Http\Message\Request::receivedRequestHeader() and implemented this functionality in + Guzzle\Http\Curl\RequestMediator +* Removed the optional $asString parameter from MessageInterface::getHeader(). Just cast the header to a string. +* Removed the optional $tryChunkedTransfer option from Guzzle\Http\Message\EntityEnclosingRequestInterface +* Removed the $asObjects argument from Guzzle\Http\Message\MessageInterface::getHeaders() + +### Removed deprecated functions + +* Removed Guzzle\Parser\ParserRegister::get(). Use getParser() +* Removed Guzzle\Parser\ParserRegister::set(). Use registerParser(). + +### Deprecations + +* The ability to case-insensitively search for header values +* Guzzle\Http\Message\Header::hasExactHeader +* Guzzle\Http\Message\Header::raw. Use getAll() +* Deprecated cache control specific methods on Guzzle\Http\Message\AbstractMessage. Use the CacheControl header object + instead. + +### Other changes + +* All response header helper functions return a string rather than mixing Header objects and strings inconsistently +* Removed cURL blacklist support. This is no longer necessary now that Expect, Accept, etc are managed by Guzzle + directly via interfaces +* Removed the injecting of a request object onto a response object. The methods to get and set a request still exist + but are a no-op until removed. +* Most classes that used to require a ``Guzzle\Service\Command\CommandInterface` typehint now request a + `Guzzle\Service\Command\ArrayCommandInterface`. +* Added `Guzzle\Http\Message\RequestInterface::startResponse()` to the RequestInterface to handle injecting a response + on a request while the request is still being transferred +* `Guzzle\Service\Command\CommandInterface` now extends from ToArrayInterface and ArrayAccess + +3.3 to 3.4 +---------- + +Base URLs of a client now follow the rules of http://tools.ietf.org/html/rfc3986#section-5.2.2 when merging URLs. + +3.2 to 3.3 +---------- + +### Response::getEtag() quote stripping removed + +`Guzzle\Http\Message\Response::getEtag()` no longer strips quotes around the ETag response header + +### Removed `Guzzle\Http\Utils` + +The `Guzzle\Http\Utils` class was removed. This class was only used for testing. + +### Stream wrapper and type + +`Guzzle\Stream\Stream::getWrapper()` and `Guzzle\Stream\Stream::getSteamType()` are no longer converted to lowercase. + +### curl.emit_io became emit_io + +Emitting IO events from a RequestMediator is now a parameter that must be set in a request's curl options using the +'emit_io' key. This was previously set under a request's parameters using 'curl.emit_io' + +3.1 to 3.2 +---------- + +### CurlMulti is no longer reused globally + +Before 3.2, the same CurlMulti object was reused globally for each client. This can cause issue where plugins added +to a single client can pollute requests dispatched from other clients. + +If you still wish to reuse the same CurlMulti object with each client, then you can add a listener to the +ServiceBuilder's `service_builder.create_client` event to inject a custom CurlMulti object into each client as it is +created. + +```php +$multi = new Guzzle\Http\Curl\CurlMulti(); +$builder = Guzzle\Service\Builder\ServiceBuilder::factory('/path/to/config.json'); +$builder->addListener('service_builder.create_client', function ($event) use ($multi) { + $event['client']->setCurlMulti($multi); +} +}); +``` + +### No default path + +URLs no longer have a default path value of '/' if no path was specified. + +Before: + +```php +$request = $client->get('http://www.foo.com'); +echo $request->getUrl(); +// >> http://www.foo.com/ +``` + +After: + +```php +$request = $client->get('http://www.foo.com'); +echo $request->getUrl(); +// >> http://www.foo.com +``` + +### Less verbose BadResponseException + +The exception message for `Guzzle\Http\Exception\BadResponseException` no longer contains the full HTTP request and +response information. You can, however, get access to the request and response object by calling `getRequest()` or +`getResponse()` on the exception object. + +### Query parameter aggregation + +Multi-valued query parameters are no longer aggregated using a callback function. `Guzzle\Http\Query` now has a +setAggregator() method that accepts a `Guzzle\Http\QueryAggregator\QueryAggregatorInterface` object. This object is +responsible for handling the aggregation of multi-valued query string variables into a flattened hash. + +2.8 to 3.x +---------- + +### Guzzle\Service\Inspector + +Change `\Guzzle\Service\Inspector::fromConfig` to `\Guzzle\Common\Collection::fromConfig` + +**Before** + +```php +use Guzzle\Service\Inspector; + +class YourClient extends \Guzzle\Service\Client +{ + public static function factory($config = array()) + { + $default = array(); + $required = array('base_url', 'username', 'api_key'); + $config = Inspector::fromConfig($config, $default, $required); + + $client = new self( + $config->get('base_url'), + $config->get('username'), + $config->get('api_key') + ); + $client->setConfig($config); + + $client->setDescription(ServiceDescription::factory(__DIR__ . DIRECTORY_SEPARATOR . 'client.json')); + + return $client; + } +``` + +**After** + +```php +use Guzzle\Common\Collection; + +class YourClient extends \Guzzle\Service\Client +{ + public static function factory($config = array()) + { + $default = array(); + $required = array('base_url', 'username', 'api_key'); + $config = Collection::fromConfig($config, $default, $required); + + $client = new self( + $config->get('base_url'), + $config->get('username'), + $config->get('api_key') + ); + $client->setConfig($config); + + $client->setDescription(ServiceDescription::factory(__DIR__ . DIRECTORY_SEPARATOR . 'client.json')); + + return $client; + } +``` + +### Convert XML Service Descriptions to JSON + +**Before** + +```xml + + + + + + Get a list of groups + + + Uses a search query to get a list of groups + + + + Create a group + + + + + Delete a group by ID + + + + + + + Update a group + + + + + + +``` + +**After** + +```json +{ + "name": "Zendesk REST API v2", + "apiVersion": "2012-12-31", + "description":"Provides access to Zendesk views, groups, tickets, ticket fields, and users", + "operations": { + "list_groups": { + "httpMethod":"GET", + "uri": "groups.json", + "summary": "Get a list of groups" + }, + "search_groups":{ + "httpMethod":"GET", + "uri": "search.json?query=\"{query} type:group\"", + "summary": "Uses a search query to get a list of groups", + "parameters":{ + "query":{ + "location": "uri", + "description":"Zendesk Search Query", + "type": "string", + "required": true + } + } + }, + "create_group": { + "httpMethod":"POST", + "uri": "groups.json", + "summary": "Create a group", + "parameters":{ + "data": { + "type": "array", + "location": "body", + "description":"Group JSON", + "filters": "json_encode", + "required": true + }, + "Content-Type":{ + "type": "string", + "location":"header", + "static": "application/json" + } + } + }, + "delete_group": { + "httpMethod":"DELETE", + "uri": "groups/{id}.json", + "summary": "Delete a group", + "parameters":{ + "id":{ + "location": "uri", + "description":"Group to delete by ID", + "type": "integer", + "required": true + } + } + }, + "get_group": { + "httpMethod":"GET", + "uri": "groups/{id}.json", + "summary": "Get a ticket", + "parameters":{ + "id":{ + "location": "uri", + "description":"Group to get by ID", + "type": "integer", + "required": true + } + } + }, + "update_group": { + "httpMethod":"PUT", + "uri": "groups/{id}.json", + "summary": "Update a group", + "parameters":{ + "id": { + "location": "uri", + "description":"Group to update by ID", + "type": "integer", + "required": true + }, + "data": { + "type": "array", + "location": "body", + "description":"Group JSON", + "filters": "json_encode", + "required": true + }, + "Content-Type":{ + "type": "string", + "location":"header", + "static": "application/json" + } + } + } +} +``` + +### Guzzle\Service\Description\ServiceDescription + +Commands are now called Operations + +**Before** + +```php +use Guzzle\Service\Description\ServiceDescription; + +$sd = new ServiceDescription(); +$sd->getCommands(); // @returns ApiCommandInterface[] +$sd->hasCommand($name); +$sd->getCommand($name); // @returns ApiCommandInterface|null +$sd->addCommand($command); // @param ApiCommandInterface $command +``` + +**After** + +```php +use Guzzle\Service\Description\ServiceDescription; + +$sd = new ServiceDescription(); +$sd->getOperations(); // @returns OperationInterface[] +$sd->hasOperation($name); +$sd->getOperation($name); // @returns OperationInterface|null +$sd->addOperation($operation); // @param OperationInterface $operation +``` + +### Guzzle\Common\Inflection\Inflector + +Namespace is now `Guzzle\Inflection\Inflector` + +### Guzzle\Http\Plugin + +Namespace is now `Guzzle\Plugin`. Many other changes occur within this namespace and are detailed in their own sections below. + +### Guzzle\Http\Plugin\LogPlugin and Guzzle\Common\Log + +Now `Guzzle\Plugin\Log\LogPlugin` and `Guzzle\Log` respectively. + +**Before** + +```php +use Guzzle\Common\Log\ClosureLogAdapter; +use Guzzle\Http\Plugin\LogPlugin; + +/** @var \Guzzle\Http\Client */ +$client; + +// $verbosity is an integer indicating desired message verbosity level +$client->addSubscriber(new LogPlugin(new ClosureLogAdapter(function($m) { echo $m; }, $verbosity = LogPlugin::LOG_VERBOSE); +``` + +**After** + +```php +use Guzzle\Log\ClosureLogAdapter; +use Guzzle\Log\MessageFormatter; +use Guzzle\Plugin\Log\LogPlugin; + +/** @var \Guzzle\Http\Client */ +$client; + +// $format is a string indicating desired message format -- @see MessageFormatter +$client->addSubscriber(new LogPlugin(new ClosureLogAdapter(function($m) { echo $m; }, $format = MessageFormatter::DEBUG_FORMAT); +``` + +### Guzzle\Http\Plugin\CurlAuthPlugin + +Now `Guzzle\Plugin\CurlAuth\CurlAuthPlugin`. + +### Guzzle\Http\Plugin\ExponentialBackoffPlugin + +Now `Guzzle\Plugin\Backoff\BackoffPlugin`, and other changes. + +**Before** + +```php +use Guzzle\Http\Plugin\ExponentialBackoffPlugin; + +$backoffPlugin = new ExponentialBackoffPlugin($maxRetries, array_merge( + ExponentialBackoffPlugin::getDefaultFailureCodes(), array(429) + )); + +$client->addSubscriber($backoffPlugin); +``` + +**After** + +```php +use Guzzle\Plugin\Backoff\BackoffPlugin; +use Guzzle\Plugin\Backoff\HttpBackoffStrategy; + +// Use convenient factory method instead -- see implementation for ideas of what +// you can do with chaining backoff strategies +$backoffPlugin = BackoffPlugin::getExponentialBackoff($maxRetries, array_merge( + HttpBackoffStrategy::getDefaultFailureCodes(), array(429) + )); +$client->addSubscriber($backoffPlugin); +``` + +### Known Issues + +#### [BUG] Accept-Encoding header behavior changed unintentionally. + +(See #217) (Fixed in 09daeb8c666fb44499a0646d655a8ae36456575e) + +In version 2.8 setting the `Accept-Encoding` header would set the CURLOPT_ENCODING option, which permitted cURL to +properly handle gzip/deflate compressed responses from the server. In versions affected by this bug this does not happen. +See issue #217 for a workaround, or use a version containing the fix. diff --git a/guzzle/guzzle/build.xml b/guzzle/guzzle/build.xml new file mode 100644 index 000000000..2aa62ba9a --- /dev/null +++ b/guzzle/guzzle/build.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/guzzle/guzzle/composer.json b/guzzle/guzzle/composer.json new file mode 100644 index 000000000..315309031 --- /dev/null +++ b/guzzle/guzzle/composer.json @@ -0,0 +1,74 @@ +{ + "name": "guzzle/guzzle", + "type": "library", + "description": "Guzzle is a PHP HTTP client library and framework for building RESTful web service clients", + "keywords": ["framework", "http", "rest", "web service", "curl", "client", "HTTP client"], + "homepage": "http://guzzlephp.org/", + "license": "MIT", + + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Guzzle Community", + "homepage": "https://github.com/guzzle/guzzle/contributors" + } + ], + + "replace": { + "guzzle/batch": "self.version", + "guzzle/cache": "self.version", + "guzzle/common": "self.version", + "guzzle/http": "self.version", + "guzzle/inflection": "self.version", + "guzzle/iterator": "self.version", + "guzzle/log": "self.version", + "guzzle/parser": "self.version", + "guzzle/plugin": "self.version", + "guzzle/plugin-async": "self.version", + "guzzle/plugin-backoff": "self.version", + "guzzle/plugin-cache": "self.version", + "guzzle/plugin-cookie": "self.version", + "guzzle/plugin-curlauth": "self.version", + "guzzle/plugin-error-response": "self.version", + "guzzle/plugin-history": "self.version", + "guzzle/plugin-log": "self.version", + "guzzle/plugin-md5": "self.version", + "guzzle/plugin-mock": "self.version", + "guzzle/plugin-oauth": "self.version", + "guzzle/service": "self.version", + "guzzle/stream": "self.version" + }, + + "require": { + "php": ">=5.3.3", + "ext-curl": "*", + "symfony/event-dispatcher": ">=2.1" + }, + + "autoload": { + "psr-0": { + "Guzzle": "src/", + "Guzzle\\Tests": "tests/" + } + }, + + "require-dev": { + "doctrine/cache": "*", + "symfony/class-loader": "*", + "monolog/monolog": "1.*", + "psr/log": "1.0.*", + "zendframework/zend-cache": "<2.3", + "zendframework/zend-log": "<2.3", + "phpunit/phpunit": "3.7.*" + }, + + "extra": { + "branch-alias": { + "dev-master": "3.8-dev" + } + } +} diff --git a/guzzle/guzzle/docs/Makefile b/guzzle/guzzle/docs/Makefile new file mode 100644 index 000000000..d92e03f95 --- /dev/null +++ b/guzzle/guzzle/docs/Makefile @@ -0,0 +1,153 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + -rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Guzzle.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Guzzle.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/Guzzle" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Guzzle" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." diff --git a/guzzle/guzzle/docs/_downloads/guzzle-schema-1.0.json b/guzzle/guzzle/docs/_downloads/guzzle-schema-1.0.json new file mode 100644 index 000000000..140595090 --- /dev/null +++ b/guzzle/guzzle/docs/_downloads/guzzle-schema-1.0.json @@ -0,0 +1,176 @@ +{ + "additionalProperties": true, + "name": { + "type": "string", + "description": "Name of the web service" + }, + "apiVersion": { + "type": ["string", "number"], + "description": "Version identifier that the service description is compatible with" + }, + "baseUrl": { + "type": "string", + "description": "Base URL of the web service. Any relative URI specified in an operation will be merged with the baseUrl using the process defined in RFC 2396" + }, + "basePath": { + "type": "string", + "description": "Alias of baseUrl" + }, + "_description": { + "type": "string", + "description": "Short summary of the web service. This is actually called 'description' but this JSON schema wont validate using just description." + }, + "operations": { + "description": "Operations of the web service", + "type": "object", + "properties": { + "extends": { + "type": "string", + "description": "Extend from another operation by name. The parent operation must be defined before the child." + }, + "httpMethod": { + "type": "string", + "description": "HTTP method used with the operation (e.g. GET, POST, PUT, DELETE, PATCH, etc)" + }, + "uri": { + "type": "string", + "description": "URI of the operation. The uri attribute can contain URI templates. The variables of the URI template are parameters of the operation with a location value of uri" + }, + "summary": { + "type": "string", + "description": "Short summary of what the operation does" + }, + "class": { + "type": "string", + "description": "Custom class to instantiate instead of the default Guzzle\\Service\\Command\\OperationCommand" + }, + "responseClass": { + "type": "string", + "description": "This is what is returned from the method. Can be a primitive, class name, or model name." + }, + "responseNotes": { + "type": "string", + "description": "A description of the response returned by the operation" + }, + "responseType": { + "type": "string", + "description": "The type of response that the operation creates. If not specified, this value will be automatically inferred based on whether or not there is a model matching the name, if a matching class name is found, or set to 'primitive' by default.", + "enum": [ "primitive", "class", "model", "documentation" ] + }, + "deprecated": { + "type": "boolean", + "description": "Whether or not the operation is deprecated" + }, + "errorResponses": { + "description": "Errors that could occur while executing the operation", + "type": "array", + "items": { + "type": "object", + "properties": { + "code": { + "type": "number", + "description": "HTTP response status code of the error" + }, + "phrase": { + "type": "string", + "description": "Response reason phrase or description of the error" + }, + "class": { + "type": "string", + "description": "A custom exception class that would be thrown if the error is encountered" + } + } + } + }, + "data": { + "type": "object", + "additionalProperties": "true" + }, + "parameters": { + "$ref": "parameters", + "description": "Parameters of the operation. Parameters are used to define how input data is serialized into a HTTP request." + }, + "additionalParameters": { + "$ref": "parameters", + "description": "Validation and serialization rules for any parameter supplied to the operation that was not explicitly defined." + } + } + }, + "models": { + "description": "Schema models that can be referenced throughout the service description. Models can be used to define how an HTTP response is parsed into a Guzzle\\Service\\Resource\\Model object.", + "type": "object", + "properties": { + "$ref": "parameters", + "description": "Parameters of the model. When a model is referenced in a responseClass attribute of an operation, parameters define how a HTTP response message is parsed into a Guzzle\\Service\\Resource\\Model." + } + }, + "includes": { + "description": "Service description files to include and extend from (can be a .json, .js, or .php file)", + "type": "array", + "items": { + "type": "string", + "pattern": ".+\\.(js|json|php)$" + } + }, + "definitions": { + "parameters": { + "extends": "http://json-schema.org/schema", + "id": "parameters", + "name": { + "type": "string", + "description": "Unique name of the parameter" + }, + "type": { + "type": ["string", "array"], + "description": "Type of variable (string, number, integer, boolean, object, array, numeric, null, any). Types are using for validation and determining the structure of a parameter. You can use a union type by providing an array of simple types. If one of the union types matches the provided value, then the value is valid." + }, + "instanceOf": { + "type": "string", + "description": "When the type is an object, you can specify the class that the object must implement" + }, + "required": { + "type": "boolean", + "description": "Whether or not the parameter is required" + }, + "default": { + "description": "Default value to use if no value is supplied" + }, + "static": { + "type": "bool", + "description": "Set to true to specify that the parameter value cannot be changed from the default setting" + }, + "description": { + "type": "string", + "description": "Documentation of the parameter" + }, + "location": { + "type": "string", + "description": "The location of a request used to apply a parameter. Custom locations can be registered with a command, but the defaults are uri, query, statusCode, reasonPhrase, header, body, json, xml, postField, postFile, responseBody" + }, + "sentAs": { + "type": "string", + "description": "Specifies how the data being modeled is sent over the wire. For example, you may wish to include certain headers in a response model that have a normalized casing of FooBar, but the actual header is x-foo-bar. In this case, sentAs would be set to x-foo-bar." + }, + "filters": { + "type": "array", + "description": "Array of static method names to to run a parameter value through. Each value in the array must be a string containing the full class path to a static method or an array of complex filter information. You can specify static methods of classes using the full namespace class name followed by ‘::’ (e.g. FooBar::baz()). Some filters require arguments in order to properly filter a value. For complex filters, use a hash containing a ‘method’ key pointing to a static method, and an ‘args’ key containing an array of positional arguments to pass to the method. Arguments can contain keywords that are replaced when filtering a value: '@value‘ is replaced with the value being validated, '@api‘ is replaced with the Parameter object.", + "items": { + "type": ["string", { + "object": { + "properties": { + "method": { + "type": "string", + "description": "PHP function to call", + "required": true + }, + "args": { + "type": "array" + } + } + } + }] + } + } + } + } +} diff --git a/guzzle/guzzle/docs/_static/guzzle-icon.png b/guzzle/guzzle/docs/_static/guzzle-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..f1017f7e6028c14a9e0694c66a6cfbb2d546adf5 GIT binary patch literal 803 zcmV+;1Kj+HP)@ zosv>1reIXPlo1y){RjSw2_NWGX5O-#W+NK3tBQ_IN%bh7xZ1Yehws80|4azI;aWIJ z_%xhlcTubTO7Dbx z)F-R8gg5MzGv|t4=e_El4GCwW0m6?C;0bG4DRC^TH6-pa>y8_h*QBud6Ms>Qf{oN> z=Q($Dn|DINB{`Ea{)h&^x;i{)XQ{?Z&id71eOkRPqgl17&E%|dJIC&SCwnd zY4oUR`OVorB8A||QZjLWS~cr&OD?HEtM@^bIovNUC5An6?z`xDgJL2H2Mya@dku<1YUfi&QvS8KS8=~uOs!oaF z8OMF7-5yyh}yDkaCp7Ob8b;wv(27WLL#lglguF0fh3d(d@ zP%vrDIA~G}dL)X;YnCMSE4ZM-gfVsYTLItd3J`~_vw^k=W%C_MlG002ovPDHLkV1oLqbt3=( literal 0 HcmV?d00001 diff --git a/guzzle/guzzle/docs/_static/homepage.css b/guzzle/guzzle/docs/_static/homepage.css new file mode 100644 index 000000000..70c46d8d5 --- /dev/null +++ b/guzzle/guzzle/docs/_static/homepage.css @@ -0,0 +1,122 @@ +/* Hero unit on homepage */ + +.hero-unit h1 { + font-size: 49px; + margin-bottom: 12px; +} + +.hero-unit { + padding: 40px; +} + +.hero-unit p { + font-size: 17px; +} + +.masthead img { + float: left; + margin-right: 17px; +} + +.hero-unit ul li { + margin-left: 220px; +} + +.hero-unit .buttons { + text-align: center; +} + +.jumbotron { + position: relative; + padding: 40px 0; + color: #fff; + text-shadow: 0 1px 3px rgba(0,0,0,.4), 0 0 30px rgba(0,0,0,.075); + background: #00312F; + background: -moz-linear-gradient(45deg, #002F31 0%, #335A6D 100%); + background: -webkit-gradient(linear, left bottom, right top, color-stop(0%,#00312D), color-stop(100%,#33566D)); + background: -webkit-linear-gradient(45deg, #020031 0%,#334F6D 100%); + background: -o-linear-gradient(45deg, #002D31 0%,#334D6D 100%); + background: -ms-linear-gradient(45deg, #002F31 0%,#33516D 100%); + background: linear-gradient(45deg, #020031 0%,#33516D 100%); + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#020031', endColorstr='#6d3353',GradientType=1 ); + -webkit-box-shadow: inset 0 3px 7px rgba(0, 0, 0, .2), inset 0 -3px 7px rgba(0, 0, 0, .2); + -moz-box-shadow: inset 0 3px 7px rgba(0,0,0,.2), inset 0 -3px 7px rgba(0,0,0,.2); + box-shadow: inset 0 3px 7px rgba(0, 0, 0, .2), inset 0 -3px 7px rgba(0, 0, 0, .2); +} + +.jumbotron h1 { + font-size: 80px; + font-weight: bold; + letter-spacing: -1px; + line-height: 1; +} + +.jumbotron p { + font-size: 24px; + font-weight: 300; + line-height: 1.25; + margin-bottom: 30px; +} + +.masthead { + padding: 40px 0 30px; + margin-bottom: 0; + color: #fff; + margin-top: -19px; +} + +.masthead h1 { + display: none; +} + +.masthead p { + font-size: 40px; + font-weight: 200; + line-height: 1.25; + margin: 12px 0 0 0; +} + +.masthead .btn { + padding: 19px 24px; + font-size: 24px; + font-weight: 200; + border: 0; +} + +/* Social bar on homepage */ + +.social { + padding: 2px 0; + text-align: center; + background-color: #f5f5f5; + border-top: 1px solid #fff; + border-bottom: 1px solid #ddd; + margin: 0 0 20px 0; +} + +.social ul { + margin-top: 0; +} + +.social-buttons { + margin-left: 0; + margin-bottom: 0; + padding-left: 0; + list-style: none; +} + +.social-buttons li { + display: inline-block; + padding: 5px 8px; + line-height: 1; + *display: inline; + *zoom: 1; +} + +.center-announcement { + padding: 10px; + background-color: rgb(238, 243, 255); + border-radius: 8px; + text-align: center; + margin: 24px 0; +} diff --git a/guzzle/guzzle/docs/_static/logo.png b/guzzle/guzzle/docs/_static/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..ecc40ac76040183ec01ef3d7c0546a82a4324589 GIT binary patch literal 248750 zcmb5VQ-`6vP!Rvk80)8?0|9}NS%`=zNQ#IMDLB}gT3DL^0dXP+ ztM1xvs-W?Q+bR`d2oPzAheiz7sw!)>s4*tL}HY7*Z+^X+M8Va_w@R z@|@~!it_1MG-qI|hNV>2WY3Z%CzfI)<;X@XG%iVlE>)h((Zx?rqtN%Gzs_9l<3~VE zUdP}3$i0yEdPHu`hm*+n*`VWlW52;EPAyrrNTc;ae!f|>LTo+a-7T_%fmDK|dS{`y z&5u&@$q@@c$IMY*qE|z8LGX61nKHB*=z(I5CGM-*;C7Ih#y3_c7&y%CGsN1rGW{ab zXF5M!=>%$gW)-&^wIPP@(7dShgbS=TKjGa;*Xh-^OB(oszr}tmc!9VPDn6AbcsuQ! zitFiBqwPTKfP2o{mBPw?S~@d%;$hX#Ka*dkjgfM$I-8+17Ge6`s=YO>8J)$KjVW8~ z8SM2-c~_+CW!S5o1Z__?{!5<%A&oWeEuB8{gY*+I;i6-u>rDDf$J*_@@@y_vb2p3VzCM%Jj-Gmv(t9 zgRpr@$h@h&X}+nvDZLrKWg?t3J4n9Cbdr1G>A|@=Z@Y8J*F0I4Ay!9J zhJz}dFf=!F6HAoX$41tkNK0L`vyM|D+_-ZhC?&#gBjtxi4_H82bGqJi-ze%zK%ajS zQ5m_p_W-mqg%-?wSSKS zr&-ZwkEz&Q+qwrL3(0x{Nn*}~c0PN=T(;o=H(H{hzXLp%YUWyHsNUB|&%HPsA$M|K zCoBb#kk1|62d9M+M-cJK+;zL3_~+6G+fJ?XZho(-vy`Gdgd8}(H5@oRdL+LX^Cpi1 znn3n$*0?mN|Ah@`I|)rkARtVX{{gUB?!tdjMG#0*R7k}Q_^KPyPe-+xqvw_X`i0rC zbe1e}yhs)X8ahaDB4}qgA+JE577^KECG?u`j?X(6e123Uo)YFmfsC3u9}5&4B_ZxF zPI}Y$Q##M{%=PQj_iwhOX&TzyBPp5C?Y!M>w&x7}Wmf0QhSm3qt>O&VIH5cA5qVsy zpe{(MI>^YV&v0{7!gUx@!L zBx3MqA@c8f-~$z&bxr>tum6}P+x}qxA5<6Ej33y;#48*W->%93L6OI)_q?0^NA*9> zoS-OUG`lQBPhP42pDOyW(w@2rLhkiUa;@fL$L?WG%Z`-_b4kn@yGlIz_vf;EaGIo^>UKGH;&^l;@;5vTisiIbN$Ti z@U3-v+&e41N2m$%v^{eADWh!dF*R3vj}@L=+%L=~c`Qwqu{rKsk`bhBtW?>neyBWw z^?7KeEVZGAWSDyeVH1loTOodcb+v4GJLrkQPnVQOS zud-LK7DLCMUc~a8MmIj*KA^mfrue5T*euS(IGuV36E!a`5koA(N!BEH{#~6|tv94xUZD;@ zQ=pQr3L7d0ksB&_urnzayoM3KR~m>6?~}qNM-GlK&{wz&+W-hVZ z7MGayJhx$*%CX{2hco8hFILlQD$6m=*W>6??AxWd=DW?-bm1xzM-&byTY6);#t<*hi^NR+w zTU#RE%z_PQYM{K!az*kY$&?Om8m0)~@6~@OQ2nUQ9=Ke&Sl6I6JOtz@*AsQC>b?KB zwVe1UZmU#!qf&5g6SZvZfR1z|bgo3mgY}=dS&kv9*HOnBIbrThot%bD5avXg6RK^9 zu{s}(n4929v_!Jmcv=3y&8}f2zLbP%H#&-GGKxp0RsfB5eb}^>^~mR+c*g`oN0!7v z2u+HRVkS&S{FXMlO4EGyt+U;?*%SXOZU3}H*e{dez~6~r;(kKoKaZb2Ajx(Yd*Sr!cblmw0geQxHwAj zjXZ4txe9fiwRDQ85E(|QtR(~>_!+8Xsa#oS;2+qwT)25+J}rw(D><_kkwq$13_T9_ zcoa+fy{2%mPSm~7cHO^ps-ChEH{#?oGi{5H%To@%-bFs292GtFB!Y9_E2}kCP9_tp z=8c~kXJgCZy|vqi+eZ{P4yiqzLa*(cgmrfE)j^KCC@rEEqPX*vJwAOTX=J*5Ybjz> zM7Wh=*-E`+a3NqJ#vOmsA3l1}M(=EZA zNqJPv57bL8+Iud!Gr@vV3W#D20m(1_Om{m@mrW*9G^N1f*_{#8+Q90gCWZUv=;l${ zBfek!&J}s+BSf?Kxtgyp!Gr|_u^9pq;44~+Jw;bw@S%byDFp|mI>z<T2N~I&xVo z^pwsPp~K-^|HXx0NkS$bDyc{rk@JvQQa|?NFBVE!qO&x6S5Sjt6%(g!I!sn%OdC&i z2L=w(M{1qS;H&moLpzg}2nEK(d$a#J)@u43Gv_OhcskP_)=d;MSVXesqv` zxQ)Z%aT0NUE+fC1Dy7y?83k{T3h82TpAzVkW@8ISem~Ot&vLDjd)tLx;1GIL zR?V_E_R>moP1BZIXc4rVTa^+A3KMeeTl-0Aj_p5XL)Bs#l9V1^4`s(1dOFt{VV%0y zve~JHc)MxUB+jb1+PlOKf5h677XhT#e`qdt8o4O8Sa(mYyY~sJ@w!W$-W@)qSlO~Q zMucOf)8T2FmFnCT0w{)Nc*N0}^E4(b!IE;ix{An*v|7v~obWK<1gzKbOC)nx;5;d5 zovFMW7+AfrJlZI8rg)uI0Tu$>hLu_$8)&_&)AbQU5Rm&pc7~Bba06XvwCuVCPkb=( zy)z(Hbo^B2t2>fkxSNK~DYWJ-2Iw`Z>AKddH`7q}=NT55%mc2H1l)0xFsGjZJrXjL z07bAr^c_P*1hKECWuHP#uVTXE(NS0e&Z{d3H1egh8~;FH1?KG*`yKwRMB2!AfMW5) ze0}~`M^HcuGGwWaj=AZ}8}RU-VTyI$)vA>x|M-3Rj*+< z%=~8%wVOaPN<@M8G&EvvSugX>*FfgIeDW0-wFo1Z2E!3-?&tvQ^z5KUfjQ%$*u!$D*{4~aOUMHJcNo@ZwJ^m0nEW)^VA9ZsvseW zSNGCLNjK8>H!C9TWm0s(MaHdCT3OI!qLkCp2MXs}z!qdJOvS>veTYyryW-TjqO#v! zd7r-rh5(A;HMv^Nh3%DEr=vv^F*HglPnz$^0Rdybm>>+U$mNwkvLH#M4qsXlv6!FC z}SHN{Srss{T_GPFNziDht?4)(ekd_I2cQ-in%O;$T+t`UDfr9tHR zS)Ako!E4-zx|t%r%%)WK-L9s*$7gK=-}8@slKFN7I%`|!Dmrb_K*m(WR*8ymnIcRs z+0VU{B$Bj~4WBO3{V)0;&daUMrJSrk!=sP?f^a?^7 z*Zr=fW06K9ybt^D8ni%l{mLM%D^LMz$>!lT3j--?IJCb4P{z$?tj|`P z;U)0jx|rm#9%L?zDUCf)MSqpmP<+VEo(&aG*b1~ZTqW)6n)mga`X1T{VBNHHxqADy zp&bc-1@`l5M;E1OlcyBAzBrV>e-b}*)Y-&G-N|PQ8^)9|FbOvGZhU`I3d5pPxj!f- zfXQ1JvG6=#zzE27HILr{p0T))Gmj)?)8e=Qd6(fHeYHMQpc0W$UKZY?`_Q6x16-TU> zS6U#XB1ffd=Eu@JB89H7p@{!(MI_m0cCnuadm8oq6z*~M$%fcr|5lSNM78#cb$hK; zd7llxi?<-cgOlf)AsAR+|ICOm=`vno4R6kET)=pWEP_dvhah@#DT>xsXE5{~A`Kpo zW?eR=Do=W7E0LHYfAE7*elKs=`-eRB^%o~nB0f|c49;-P*gHEbBIU z!3+=$I0Hfvdwve4pPvW$4@d93MWHbuS^nAFqYB_3(Wr6yx)M1W5BBw8m>O4C4SNJO zApW?z3S3*WOCICakeqgX#i#bIQ~57ed!66x=*CjJQMZl2qU9c!rU=*b5i+zFiEOp- zgxeS^W5~X-&_5q{bQswckb+dz)^gn29v$v_1qXBQw!Fy(;LX9#bN>3DTIUeE!dI#8z1Kp2>;`s^U|_u?W=9Tgzp=PmpXkg1_m47_f7 z7_h2~cSsYa@V!w#W??{c`Xh}hZ}@ozqiq&hj#4la6|{>z&>$KU=?4lpRI@CZwR(~t(?Gs3Sz8d)uXcuG+U z+qs5u{)^XVvqHX-QX*|RDp(Q8&@e2l6h!9ku~h6prc)|BIpYyiiUWtVBxEyBo+>I! zXvvin$kiw~_qSL^fPK{8Z}A>SU4IImuoF0#X+&*NfK=eUxUi;1Kz`6!Zor8=M^T^1iASR@ls z_StlFE2AzDYmNKmbMV}$zItBMFG*wF$x!hA-9?k4J&IX6iefsIp_-Ud44b#Rw~}%1 zb-N$Htc7Gj$+kW_cf(3+N|-DP(i-sOSY-bDK=vB8is|mTCm(|zTnQpwOAQf|7Uy}} zoeE*>Lys}+kQtBHT2)z=N2~o7ek1|~W;6Py&<1C;R7Of_M2_^ApTWQJ9|#D+-6FG~ zzKZ7kQFtSra@h*>ZHtptodWc2D|yCFCa$&bcReAc&Cecu`vsFg83mEG>yc7^QHg3x zar5}RjEh`o1p~y{uchdHEjdaj%Gz3_ECpRWcDEmB_goHRV<_lD-klV_ zX>(L0f=(!@P4HtK>M_xaDa$IFnP)!S74t+2S?+$m2ep9fsp5ew*i~gY?-CPoaLq5r zWcisZWif%(DibJpITdFeXL;5o9fm3?`BAK=ezkmgG)Uf#I%W8cYhlU$9go_|O)R@( zze=bMOBe_S@5m^o&`iTIL*>`@<28-El0h5lDE-$n!yUe|Cb=_d zckywd1XD;gA#sx-mm%tyryl0Ql9L;Rm{jjk`(P-4JQWy?e(9id01&O+ep@=>&ZZEk z#cTq-zn!3K?NPcs3%^eVB$XSr#dt?XTv*3Hq-2ZTHF~c zS;|Td3&DV*?k_7{5VJ-wxy%RWq|yE!V$C>-tp;~iyi3d6`^nHof$oonr7MLod(x~s zBkPufMty{x2*I^y5K{I1XB?_Zt1`jUgHT>w6mwvoG<0)YcK%sV6nzA^4Qd0bPFv+Z zKF04dhcpoIf9HLCEJmw%h3a= z+^nQAL#>)5g~<}*2!kKl{?|WFc`z4VCd-GLsObeR+eJD|?_P7Vby4G*u04B@P8uA~TUVmf@c~~#ck`5&693sayIr<_Tvc{oYR!l*nicdvCc=0i7HB5-Rv>o zYP08Rz~mm#BX-fI#F^l6|M8_pB5B+_)iCEK-{Bgg^RK z`(ka-jt*pVyS)F3l(LrV1B`FNiM;&lRt>%{%p=Q|s!T4ZFw$(I z``58%+d>`MQ%vq?Vv}V*BWDTAwB3fkA&%`A8!#gquz(i~h!dDGkTzq8HS<&(EKo6( zYXLXtdf~+#E^s16Bkas4-eE-^4qZTZI=XbwMM<7j4G_ZrIy+l&4`FC@S<7gQAFsqZ z=v1E?wSP0Ny*Er8trF%^Vg`)Y3L^Ulg#Vz>*EGWABdi&5U}_+z^KV0gAhzmk)Rmu-WI@5t7b%%Dw4 zB4CAB_+;E6Q>{jL-D)p&q|vOIglBk!DD*}NhD49SK2s+ z57Z{Mm$i7b9xsCqxc@vd)POn!@oi$?nl9})sEP?;ZrB|q3H+DTtjO&~@=Py3LrkG5 zI-z^_pnMdv8?+QurxSoOzsCy}_JcqdHl(z5daV89Stj@*?$RXQS@$gI(&t{UZf(8i zp|!aZz90R8eJ$pmnf&i<2SRcF-j&a@zj#ZLF_kzNJfQgy&nf(&7ln2p@jAUkRyQXN@hXkFtlk#cC2U!gTr*h5iTRYvNT=)BW%2loeHT zNe0Ash=~(Wh}MJMNGZVcC(I%-aO#J8Q8QZ zH_>W?M);CMC-9k`!W;5uPyG*zZ$n9w{%%AetgVs2J(~%HFLqo|RwqzjAm3zfL=1XZCAF$a-Oz6J)E!ADR2QRLxzyD(Vf!gHM-}-2d5jlMLwa zdJ2yqyvaZB*bL;jcJSYIqmyTdOt2IM#NWn+iuh(H#i7OThkbmWKgXX2^*C|X zs5Q9F8?}(gaLN*(K4s8V6fq^xl3Nbjp{}E|^s<5Ye(~^q!G3PIe_CZ7S5|t~f-g4~ zTlYR94YH*?AqQJwxAwrfLjc!$I@zTAo_$4(_18zV0!t;VJrPU* z1n(qJ6dMSrCZHvSw@4jzcaz`z$e?lu0*(ljqC)}$13mGJvFe9bfglyZ2L!Co2AWeT zCC4<0vMxVf*vtB~1fci}^|*J8A``@XQi&rXG>A0|PERZgT1|_j?Ji``dSz3R8IGAp zzS}-@$$E@|_&zh{R@8WzL;g>WECiD)(cg);=S~+V6`(H-JYln1xFeJa24|0EYJl?n zhX}PC+wu7iA8M6pG=h}gFOVIayYGF3BvH_Sv!W1X_4cdr2ya6UqrN#Z(d0MP;{NHU>|=NTX4)`Wg=h+_(0jg97EnFC?F%h59MF9Y@S zGXp)%*6GFC?%w>`Y-7hadSJSqAiEYW22>Sg)rf2qI2$Y`HR&q;FLlII&dR-g6-vj?iGx)=-*jvLl z#{o3{`+s@|2_qUFnu`8RbcvI^aKo5=-*e&Fp43JqH>Xh`-o!ibm=ai$q`O5eZ+wE6 z1S0aW)8Sxxjv;5P>;s9sUJT3rT0$sKKUuQccD-Lv6Av6V@zV)uw>zg=ha6Rr6gNTw zyP_zfZ1ez7<7=PQs&E9E=qE=joFotOEnMQrc&&x|e!VUfSuby$QucEb{Wcm1l3Kw` zU-i7J$3Bu46WUK3oB7{3Z(|DPQ!q^o`heeFB>o@qO-}@yP6JDeOY}pSmkzLo4f49j zK{BtJ$Ek00FcDmy5YwyM3+f;Tegh_#k%a#pOH>q2x(QbuTJ>VtH0Q^RnKin>6%^A8 zq@})G&Uwb?oGb%Au-WU16{X!DWc7jjfjo*`$VkANI&79OyrOd9gsZfcAqeV`ma#u6 z)gbHFT5gl(`d#eTn+wt!3;7J9Q6+#VjPei?TU_dn(0cpL6-!egsa+jOaLxDVtO7G`M(^Gd)(!nYtH zFHGt%um17GFJAZ)6oxy_5dR}UsACyWc^{am_pkFbST*xH~p$I^~E ze&y?MiVQTT%Z>lC76MHttf=$sK{H5a=%#gwx7~TQtDGKNs>W?4-5KxFVw0Tu8zMI5 z4~tEPFPP+PifW}=|MQT401;!;^Wc2DV<;wsSxeiyPuM^-oswuSJA9YZK`R8{6EnKw z>kai2vS(**Q8{|?L23;e|n* zMn@1V+sJ%%Qee*dWxC)u?%CI1jv$E)rZs7)e6$a^4$n~cMCCvU{=xL}Q5T6HmZehA ztoL}bMZn7OH)m> zLG!K=pGvaV>z}Hby9ekUwF#8P1>^RIXn|&^O0~gX29@!Hc&cV{ALJrof2wP2_#iwY z!z>`?o{4iF=7Puelb372jk^Y|25wt@$6&I|atwbJ>>1T<(AjZau|!?zA->{S+b3zF z3fA&`D(C(s*i1Ee_D#V(h%L7NA+*7jkhQqFffNo*E{Mdo+rV4cc7YuMn5R$G5(NU+ z?)7{oav2eHVN%nM=aN#rlx0c4={PT|W)v>P<`&XIbJZo#xsE?A!W60^8IXQ;{hl5` z=f|kfckI?hF$_fL1`dNYp&LU@yoYBE^W!Ri<_Mucp3E1GMZAlp3SQPL_y1Nwh8=zL zqh9|Gbw5w2RJA($i0K^0n+-`Sw71Z15#-(Xz6?1Ev^fOyj*nYUosB#~FXDjoZmT-B ziihej`&!;)Lz~Ti3YP5ebs)aOi}VLT__cgf5|QP=wgnaMHNFwwoiwMZ>jD${IzHyg zSAxma-N>olY8lm2v&o)dLQKT9Nb``8tmoEU11B%?EJgf?-&$SyK6f9EkS{foBPh)zR92(L)@+{Oad_?W%#NR|ebWsphg0k-gTy(-{=c#8Vry;Mr4tx}G=6leApT z)LjdmVm-94vf^YfIO*cD?ZnD?;ehD_mv)s|7xY2Y zrpGw{HEkkRlOxb-@(l!vSk5k7JhfFx*g1 z{ohS0JEf1Oz$-U&ftqY5GTwg~ieQrq0;>%I%$?7!?DzV<&E{ny#kK3mpdMHma>4*g z<`@LEsQuo})Xp<3tI0`D(d1ZA&$R%ObKgP%%ZI8`O2aY%@JM~N1uSb~LS)+B-=OJm zTq5hM*`n{C=rW2#%Jxmp+shMQ%n=cc zj~&&2{CA{rw?sPzdHZV7o7H5vxC*_=VL9LLpLftyD36+h-)X zp|$V(@#wRdLXe#2-#Vurssvd50w2B}nk@~)f_%Pk(L?~0?pvZQo3%l+ad9rm)VKE= zr_b?tNpH*P4TGy+7bgYQh)w37T8oyRo2`R9BVF47tT!K0{`VU(Y| zgZU8ZuWN*7@;8hhr9Tjv{z~nzoguIdI~AHxv^FlZ$E1;p_XcUn)8^J~-Obf2=(vUH z=b9{+rYb*emg@-A2B5;h0_Jb$BHdZFdzG%Z)h@vAybT9c3eNX6&EDzh2_{QecGF;i z*0(H(U>Wgc937EwTLBYW6C*Q}Xt9;SS|=pEWUM<#BMH zAjdW~OU7;PT>A-#&~z)C3?Ro60=`G&cfa(&;`?8{dfr>fmHTGKLMF@&4)8fGHke%x ztyfh{Iz}s85pup4_fU%tA^$YdsQ>joe7-)%lS{9eB%XQ!Yu!Y_j3vdwbS6T270Q^5 zvdQa4m;69lRB#B@wX`{#6KjKrmMK7H3WD#rFr`?;=#S z7Sydif_!Al_Mt}Nh0MJqnUM+E&=??mfWV<{sQf{R@$8*Dg*ulpidv z*PGgKMZ6bf-~01M!tdMHPc8!O6RI(ut-;Bx9CN6(Hiz7aOzyjfQJRxflZo7l7m8Ud zC~u}lm!rb&XEu`GmIqZ49XeLS5#HkwWNTooZKSvATDp_i)b!b=V;2&@`Wjw9L ziwe846r0|wjKyZ`Ce<#=A8fA^;)xSc?g6nSXx}EMww~~FcR$BhibUyf7lB54X~jWD z8h?hBc12)j(sj@)ku}AE%`ggbEuWqu59S)Ghrym{`trHDqB5ZMh|yVCk2?0hrXu{f zzCVp#db~+x$fWe5K5nf$N!AJz+W@juEd)1>*nxAsk5(76Ncya~{n@loHMO|3#^7PE zD?sgYY-TS5H@K%4A z8xQK3>Jo+W9^gBJ4r1)2q-1xtwi^85d~NXI0q$yxld&Ld`kttvrv!}v?`i}4CEq4f z5dmsTROl`@)^Ce#>wunW2`9CecgoRw8t2{%q1bT-(iQk2+ydLW9)wEESTd1-F_7(F zN_YR!O3Wju)G0AlnUN<2Sg%`gdXUZ}HBBOH%;!B}bI-f!o82PzcLE+aLjRzUNvx?y zP__f>%xem&6}Ozz2EJOrTA`5;bt_9%OTWm+MJ#nAWzXmK&9SSGL8z&J70b{N_|C-E zrFXjc9;4o@3{z$T%jYR3I{YL=d4E42Jp9IJD*bX6I~!eN^!rzeWv0}~-)%CLJ*a`z zhTx*anb$#Y9-~@><^sc~T9-2sq8s-?YUdt8ProZSzB@yjzIyvei6M_{+)ebe0 z1Lqla2gBhw^KVaJOnUzY%=*rEHnldksBm!o%3rGbW5zayp3}E}@eKh(i%uSVC)}g( z!nCT(=_%3uZh4RiCSc8|%RTX59g5Uz`^gExl?Coyb>($`RQc>>rY+ka0e9#xZxl|G z3H7)t!$jCa2d>Id)sr7068BE>VRTitW`Q=h>*SG?0_>a=0LNfz7%!ALK&mvorDO_%jrgk|(^RbD(*)D9j>&^h zMHxPAvZul$GMX|Y{_^u0jolhQFKS(8e!s=!@3(wh=)O{ z)WB0lkYSu$OMg2r%=#6_zP-FbDR)tH#l=H;Q@G$**|9*G7DY9tpW+WBTXa!E_U8*( z%Ic~a0vp>nx^zvg*WLRmPi+P(^mDlA$8oh*Gy`orH`*V>=^eYhzLCfh8T+pe(v_CG zrF`@LnU%JFF`sDpgsYw;c7;C8%!^g6>gYhr4o+fL_N&s%0>!+UEs8nFaFbSVAJ`EI z!NAY?1pll2<5s>+4sQ=qy%xh@^OsVE&~#@jNx=+%?=;1$>tQ}D4FrwBFkZZNOC1r| zO3K;!7AI$Xjm533sek@Gf>V2Z-k!3y_Vs*i-nGLuUCg|}pZINgH%L26%gc(!i_mwE z2y|rP)RVEa1cR@ZQ@(LmD1!x$!s4FtsHoblAj@@F$r2ZP6>FRgEav7{uO{A4uy&5x zflIb`F!#6rrIVpHYg^Y$?TZBOhA2hfWQl!z_@J-)AI@RE5}pFHCznjkT~Ln?2wvFb zGHzA%>(^Hz)H3$|J-C(dM~LRAI}O<$M)j0-K6zp!;U#eMDad8 zi+6CFI(sE7gS9P0_D*!!W#!`RVuR% zgVac`J8#1m=$c0jZks(N1t`$Sh>TTq4wDch$m%RKvPM?Pj}Dm&bd@6MnKo zaqkL6$k1l7$Qo88moKwrq{qs9zAtRssDvaMada{N$NrtxIE0 zoeoEheovX2oTig0ANYyvojwq>1YL+hSuPEm?kb?0?+AtW#v-T_rCo(tT}7)4Kc%X^ zrQ5e=dR4)}HI-LjoT85%}A;4Z}d}=yTXQ!tsKQ__dIP%r(_a6)e zgW`l2>`m;YLD3Jv{O@iM8F3EtwAkE*t6%ihk6rldxn*9}{*|wCLwHx?^<$b-PQy|MwQ@W=3W=LA(1tX7?}HfuDe8(n%+x zjm&1Wkt-T3Rzu8|-7Anf(8f|{pp6%tJ`$!C77p4Ex~$~;;(nECDXJpKM4zi4FeV*7 zFghr|7DL!|(Z;kCb{e;oY7-3Gm(QfKM;T=5?u$5 z2Y)T0X~RUfGA&v-T2k|)&7BIo^M_R7&nm6ya>k1bpufwVF>ZHDgZlAZ$W4$z>_A+J zBb>5aIl7HRl!*;oMjI1EWJ)C#=`>^CqyVu8s*fBA=mG2~Mf8+WcTGXG-ENMtq-df2 zp^mmWzv6D=KJg3RQF2aW6(cv0OLk<0$Q-w2wBPO=YfVnxmTLHdvxX0xHR$gb@-jlpGvMjtE7+E?tB?e3sEqX-P))w0gL}N z~5`Vz7a)K?%0#HaXGAao}h=JL%c^#-0HC0eF=(_|IZoXG1OZToYcDn%OXsGjB7~vjo3hp{K*L8z0@gwk7Eb$Zd54?0Kfnj7 zYiRgcV4fc6KK^WSLy$rpdhS?SfU|YFN1DhCz3cz3)GH5^d)Qd=Fiu<{b~7Q6oN?C{7_I^u+z9rP3ew7OO+% za}(Rwpd!6@vlbF}K(!x6m=ZvAR8&MGFE7)#z)(5X7>FYaX)UF`B}f#t5dV5}nRZH% z1`CrAlu}{CXgB1obwI`xwoN{Gki{0}E3&6Ms?NiWM?xLNkukOD0_I1A^G%#`C$6S( z3BT|63;BQNg}Rp5p5Qn+YGiOEUj2gg$TIMoX*FAgKdQi1-xWWMlKx3b%M zaI628&w5WEb%;>ikHz+G&HXMZQywIUZ)mLZqR~1;E-Wzr?sdb%54bNo_5_jp)V8)9 zwSTnQ8ANIOvUd+`57m4v;UCv)SN&c{Cf3<|Iu7nx@ra2UO zeXQN!!JZVitOW<0S>A5fRczkB(&_bRw|)PF3J^&)K_P5Xw>h>dvAkUcOc)eA>`!K2 zuDBq~xGH`?Yo??OT0R43LNK$iAb{2iSZNA4#b4FRqGbI|vxjZj_)eJGGYw+PMEz)2 z9}6+wdEF%Z(l;NEK(G1<8{jrk6EX7bWf&Z6T=>XS-J_XP9?uU_MnMfp$!6h6B;S#u zNwhSRls7Kem1;JNN>Dj1NgfOfM$Ro|nHOMnRvkE%+-gSW!zw)gn&!fD`m4!^7x79M zv$DF(rM4qEx&uQ}P?h%d^lsfLfF>0!W(&vhh$JZno*rY~yV zyC!KBB(;%@hXeylY%iin%LZ8iC2bFh_o&Y?lB;_?{f3mGFUG-Gu#v5>4VFwnPRa#oYQS4Lm{su(zo7>geKqwNcX7iW@Jxc(Q~fA3`^9 ze%&zh9OV}`I?eF)efdAOGgA$*opn!9%u7`&1w%~lk)e9<x!w`zOs)L4`utXBeK{t>grf-gx@+`LvLAl&!YM^32cpDzX(13Crt0vg4 z|1tW8UXMeSxVIZ@E?2>O+ZkKusSXDj7^5g7ZY&s7&+J}$;y}IWoCB`WwsilAq9iZj zTnSad9op&cWPp8sTYJ5Y>A(KK^ya!!=y%>*yvQd>pArn(0M2jrV}Udqs=E1xU7=Rn zR&*e7Kf_ktjj1sHTm@N#*9Uq#Qgfiir|QpU+kD1p*P`;T12Z#zH3EI#=f|yE??UkFoOx_?^K?A44&V2~$zpC@F*arv$+sOh8mho2 z8+2Gqk)C`$Hqnc+Ru~JCr6l?+(0X)3EpD>Z+3k-T<0gY-+*+S3#Zb)NP*UZ*R(IY? ze*E5Ec{j^BN2-ioFb40Un_sGZ5OZ+p1smwgndsdg6`=PW*8q0m9>^)9A`!L6tEZ5@@X zrI!+}tof=}ZoLT-yD1+8l}0z*&2By;RLZ!ZdHsx3$D?vP>NLw&#uI-(`5sUW@Ar9p|c*OFtVYI@@K}j1O6~L!G1wd zOaW$VrVt1th0BUnTtwdKsv=2;HCu0Ts_5wO zeH@<4{@G6Id^MTmo5t)o&T**aWaU4SOzjBcF3-~kt8qSnd-Qczm&oH5ozi7lgbAH6 zsKb{9Ub5zQi%`4?Zvt(8aE#0l==SiMH;^%PQlFF8PAIk@92QaPlX);>=h?bKL_sWI!0EQ}YU@UYdDET45-P`n zi%XlJ^5@idY>cE1-M2xnt?YPR!y#>-_#WPj9;t2CWEKV^^?X&?_6K{v8G3W z@2AcATgO$^r0i6$jdoYNpbZ04O@-38!`spI8;kTrW|m?17Z-)|*d|mqlf6Cq;(V7K z?kDT0@nzG(ou8AyMo7vA7-~F6rK?7usY-1l=2359I$NaAKuJMn;3a=RpLVg6?N-X| z@b5QjPvAUH7V+6qIjmc^7^bm|LtB^?kO>X(B9Q_0T5}LI4bUKsSjPLJpvRFu`7F@i zPK+NtRV0bZ)J0!hlx6i=x3_&f!o!d^Rijg{*v&?~x82nU@K^Tb>~I~Of80QguTh#k zM9vSLVP^HAnUJ|3_CR4sh8&3T`sx+7A@nddrvJF++|)eM6s$UqM=xQw2J5&^*VkZN z3FC`-;^WJKk$=~J@bvPNmhth#H`j~4Ek;NMRIuqAW{tqqv^eq@x#^K1})jutXdt?II! zQKk^j@KgIY!q}jmM0)k> zu@ZywsGuMPwc7n0ypMNOea(B0Q?x>=_b3rZD*{N`t2nHTVS$*&u@iVwCHcJ{uQ-%a zckjca&w=_iv53ZDMKJ|ZFIQN~8|+;9FajWG1;xfZwstHl$Zclea5Dc-Sea1)Plm3)3}vJe3ib;^Cur(K=p zrQFq0IlW;6pLamr*d~(~!MDMj%s64{xVpDJfHqE8rRaU7^O&&`6;ELU=dp+cUGnUq z{$vBgQj~-9B3MK`>Suo-pJ;ihOZHL6bfE21!r)UVfQlQ=rAJ!vAK(7yZ zC6~4N_|d)kPL|wNW>xXy+iA()$+X}H3ksM!ynnLP>iOh1M!TNS%g~)#5lJiUCVmbS9=d|r1A{0RLd&`7e~vxBaZ%jFlNdO$ z6^s^`8!&7gL8uC-WaQkyLbq|rNh_7AFdtX|4>!SWW-*}!H|E&OX~5;$5A4&x$Y zD^h5|Ma*JX-n$zqPS8CwG<&`Fq`&dKIguNV>rQ)OZ~nKy&EVqzCH{I^ja|(0*g@9`M z3sAN*4~_X^=H#4>jSknOx>Kj|Md%VPI1kapOHsvzDdLw8)v*p#v~+OZ9mlwE$Ib(E zYhxNU#^DUV3||IaeybHGY;U$Tu?UhL=lRMlo%#h?4SlJuV)Q)vaNFcNn?6yvH!reJZJ&<|jQ9z&CD6x%YHK6QTe4o;~juB)BQ0P_%|Wogx&R z{&SB)9n`6EZ^4;v?I&p2uQR1f5=(gBR>T#~+))1hf{?E#^*hUtKe^y#Wq5~)*F1gl z+3S7QeIAXvK@!%{-?}yVpiF^n!R~Y$3}J5^@W!uW7g` z{-!oRTeR?Z0qw@sLbW}~cZL|-M-`u*{TGH*#bF*A8;jzpme(rX_r5!jJhoT7gNSb6 z^dry;Axr#N5T>`3p9qjpsX!j7P2vSe#Uut*4XDhJUu?L5=dtas-BW09a4mlZlIJ^l z{C^e4)mW!;$J!5oZ*LU0%`JqOf$P-3Mg}a7KVb{aVzSZS@~f-p#r^$pkz*6i+-xpu zAxhMY-oUg;;d|O+0ldddBP5&sVWVL+b0IXA1O{SCw~^uID>j;nVM(45rNvwQQxtM_!kDN$W(d0MD+8%~5d8MKY3kobsijRl$;jaDws^2b<63J zQ=(vfg?K~X*9W?vR;m311Ue2l50pbW$qiuuOk?MvI)Y?!bw&iy6ttD!GQIUZ{D=Ma zGk4!peNIL7hiuXP6@O%6RU_y?9R-33^N=sP^47` z*)#YhIzo#0!c~>wJx=+csI5+R+2s3k%j-VeHzDSB6xwl@o|caSEof79^;JPKx=?*( z*z#VEj83tSdkYoUgoGiu(phe0Gs&0gijLIZxbC*k)!4dpCnAp}mTpNpZ?b{K!#}n zkF3jtWRbC>5EOZl?aQ1}L0LPyj*zfJ_&svUNhgv2<3FARcK7yrU6Stxl$UDfy1px} z=s@#UUEbW=yA`wTbJE5^xW9c(RP6pc|;*ePj!xTSsn z5HFk09mNy5@G?|0vZq)g`-V?frY`!#XCB6N&G>g(pEa=3WwviG0Pp^GLLzgA`~J|} zr4tpGd@gAD`cza~$;HHVuhmsXuRSt0q*C)w64`2!Y-P2AI(dd*?m-Fcdnj|2oLlbS zG(7)M8D5S(s1yVk?OxJxG$o@G$z3tT=elpP8Bv2sMx*TO< zFUY0h(_o>Xf(ro;iujVj;p@$${;UZk?k@_@Irp3eSL}!mjdy&+$BSIDNRL6S!w3|- z0NTV?HJ3ko#LzkZ%>3tnbP3EVDKP*gYqtb@(4;nsb;!$HvM`Q%y^iHOa@dz5vM*%S zpJxRAZ+@lFQJ$lU*Rf_rD%4&)-z@+DKmbWZK~#WMMDQrWzKW9hItpjXmGzg+5{YoP zB(RWalMvYSF2Ra_;r8uEkT)ZZpRQMjL})ImG&}u)2SW_Y!g-8aGf@?3mTa)YG;qlBA zZ1w>BR{zc@yCZ3_`=*vnoY8Q!g#5^mL>6zsPbc95kvB?JBqgqfpF*XHk?-@b|2z`F zq6_6~rdy^&y@Qa#G1h54I6_i`UF+vpQYR~N^jNa&HV;mmpqiZF zU!&|!2a3Y6e=$_{AOcj^NCMrgF#2WA%`f61TL&e0MzOGtyMa%cbB^qmqG^qru=V@Q zTx#!ibLWm`-W8-n6y>i9MDD(H(lX*lStVDIUjed#G#g%53s4-Ri;p9SvJ00^e6cIO zc-8i(=aQ)vgNmeYBZ6p3lDH2RQ$fU3E_!i)3Vd|=<8({llqfctu})pI_5*`BBr?OL zC*8v)3pO6lfz+*JIMBtm&H`ytB^Kc~2ZPxe+6zIs2!Iaqts8g%x_a#gi+0_YxvGa5QJP_)6{&_@c>Q0d4*;>P&qn=q>o7VX@m#R_oX5&Ylyf zIuo`atETl0Ex-g!nc2N@Lt0hWqLj?EGAVG3 z@^z>v6_F$bP9_37v{=zCI@sGgcAI&&$SpsP(;$J3EiFArHSeXSiHgaV8mO#1=6M+z zg0od0c|Y>%D3Xk6Pel;e59^kps>#O?8NMQ|8(UEZGzN&wKg z2-1lEWJuOu>g(Sqx#hIy{05V%aSQ$W>Y8{6D&X*@LJ?367o-hN!Hz)`&NlsOk~A*2wl3b;clbu7dgAG&=q z*6xqNat0np&~3`PkbeA4lS%h>4Yf5~P*c-~Hiz-D&Na?)hNnx?ir$Wl>2)?ro{)6r zb!6k|CDgqf>C}s$Y$K?0@)duuXi{HWy3^3KpJX)kK3`D&5pVOqcsvEGGauocMdxBy z`YGV*A4jR!|2B}Cj7lLZKK0<@LJ;?uyC1%PK+B<=7SA3Eg#?9b=_1JUh=2G@3*(=l z|F_NU;6$Zu=D7<>H_Jr31vUiL3@uSu5!qOaOr85c-n+0Pem#o!E<+G4h|)7*tgRx_ zL4Tgpvd?d*DeGJ@#!t1mDHcH0_X`Xn%>ZfoxgbYrQf&4R%Hj0?6Wovd6pxcYN0yWu zTJ{E%k}EbDZ=D+QKY>$5BlHce{iRG+eurn}IYZCXo(mn6f#v%L z)g-^>hIR1LnsCDDqTygPlH`&W@kqs%luKHm;YllI@?`bkjvLZD`q8Q4Ov)v3X*iVF z)sv{U1mRnTVf{cy(LbL{{59ZZzZHl8;y}A;56Q0iXh}(C?0_7LzYYZUg$D;qlQMe< zRidYRZT?xQuy`{`$1X59o8J^Y@WVUi&v#Hn$WcL+m=>;};!~}t2wq7cv4eK5`}lUp z_K|;oY9w&NcJ1$N_Q1Mz>hwjI{1Ln)3BxcKz>z;uCUSSgry-1$%@dD^2E1SKQ8y zpm{Xmz-`^OapciB*(IoQreRAKtpl8@>9XsG^uh(cvB=-`Ar-|D$;diJM z1Rmukd(_J1e&VVw+OjC%K870_ZSuBcE(Cw+EI1otmZtYPWs0(l?_PA+`wQu_OHv| zGm5YnJEr{mQzwD9?ejf46x$)w(Scoo>tTY9l^Ky_r*_)t&2?N5zV7aLd!JUFGR=Fz zUN#QP?w?`MP`6k+>Th%Wd^u*80wM89pD10e2nurZjRBKLgMEDs$M}d(ofz^hoy`)c z2I*b>k!MM8f=JF*dCYkE#Z|?JEiL;KQ6ZlNwdAeH7`r1}<$v_zNo9=-u+EP;cjPU{ zmJdia^Pu7r)@n**I}CzdqR%{dJXCAsf-~{z5Cdsea^14Z%G|DohK$VZZ?j0?v6r^K zyc((btHA6=<-x(RvLGJCZ1F16&*1CbN@Y8%T3uaPF}tooU|#8tT|{i*p0sT-$rc{;x%mFJA=J@|2(1f?B57>3hL!c&&iE4Wu!aTD_3Nf~ zK$q-jVz30I-HD^n^T$7#1RR)^-xZ!~Ybuh|y@|p8!T1H$rEL(y=;H2(#CgWBjdm2u z-*iuHM=5y{Cd5&2&b3Y-tIEpM6=UKL&fm|r!RSLE2f*wUp-0mgG29Y+oa zZ;TzSaPRc#PO;a~zmI&Me?2u4IA-O0yG>Hv%yNQi!h~im)0#4+ux=EkfP2W`ulg6x z_&Ew8uZc3ZQdzxHcC%ZvS$h6n{syAK()mt_*b&Jls8=OlDo~#fig$Et^>)XZzy94N z0YxY%)4Bb9?vfrdRPE7FIPl@7CUOG)`>3M$|JnNz_^PUF{XLxN&NuTMLI_is31UGk zLC}g*Yy0p$+iGq5ineOCeXq5HtaS&AsQGyZ74rtiASH-}=@`74L}_^TE335>UJeq0EFd zL?!LhUGa)`Zr$G#(xTdpE^}!fVUJI%u72BdAq<}SNAB5EF)8PO=#zN%!ieDox%6x& z>D{VY-ag>%&tZ8|j#)W#$)@&r9v#MQ@y*2-jeA$H^q>FG(4IFaAMx8`$H+=xM3rw) ziS~lI>Rn=d&ZUU^Yp3Hi}v^73dawW=%yw(cr`uR*7q|KQYT&g<5St#(w6f zskr$wPMq7Ybg#?GB|~Mqd$+9-@B0++{ems9!n3sW!J6aKCjW6F8N2i8CIQD_KJ1)9 z9pY_GrmQckEVveJJRbG%_D1u1a@0Crv0{aSa#mmJ_V^r;E!Nc?`y_DZcWX@k4yeO^ zIPLu9Q&yee|9t!HPLEHv6cnWU`t28{=BfKnwA6=LsbVo1T2REZm$cINCSCVn!n7|p zEz+M(Ti6t@G#6S_ZvvI$UA4q^{}8c+`Fhea?sda*AVqdRx~V40nh3rV11;PUAu

nuab2?k-|w*ozrU)a#T6yG9Pd{$ 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:*n?Sog9y!Kl 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

ZjUB+p_8_i@3V<~Wo>c?KnkDG5yPKDk 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 B0 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 qT?<>l>uJ+BhVT`s90gN-O10`YIOzj`~B z0+&guI2s5;>Z-DaC8Xjup+2=(S#WLB2Uw2kuXgG~;6JkN{;lH=&2S?EU}KfPkE>nM z!EQ9&;%IB#Qj#-0eLTFYM~*EGp+_(_rGla^RT;TyKvq_KlRwgvl9a9ynOSixy(7$v z_D)J17M23bIfo+|T(?i-QoW!z-dP@~S(2PwU~y96i#eL>0(`H`)ME>}r>2sKiZ*vG zlBQ{A9TV{NozRgwBH8zHPnzQ^K+7z=2L z1!hgnfEKw2E}?SVe=Od2hB^2u%J)3c3#Rh!rqDgmTJML}x(tGI7c|H#aW0FpJNMN3 zlfDIX)7(zd)?YeE`*M@Bv+>&jkda-%V1}A(hELWkOJDiR#6!0}|JL0$ft>+%vu`*` zdD3K&+HO)GAnE}jS=#WSStlrwPZULn8JTEez zpf-FicF$W8;V=|!d9o?&eIfDtAFD!xY2Lg5+IJ?@wcQp|S@hJJ?+y?;(_iZQG2b!! ze8H1rG&_tn^)$sFSP7rq4U)2^<^pShz`5-_Bd& z@pz1A&%(5HI|>HS^aU{8@{n**Yh~F*oUXnbf+oi}+_ywPh6Lf2nT(igP>Y%YSy7{^ zS(@$QUJb4DehMCtmi79-G$=&g6#F7qf!1Iaj4G1QL;<3l-YeNbtSqW75=jfF%ydkS z8=&P1_!@!~sqHky zc4Nl$Q7P*qfCiTC7{grxb5=)1V@WSA$*5W94p;`gI;4w^;|hD(8I0(^|} z+AS71sLZ?ZkkfQkd%+T?Hxt22PU~QA*A6X*H<{vv=kMZ$@v62u!VdJku^t(etMx%4uSBhhD^VM6X_=qE>*ub5aa?XT)#-3UkoJjZkh&{G?y{5d`%bBHt8*AUCT8(~nH$RTE3Jo}y&uU|c7%9Q3Z=#zV- z)inmScfqD}CGgF&BNBC~C8Eu?psFqJPM(7mcDu;4Cs`T7u4sG{k@aW8s{SV6Fwohu z&vAp&rY}Wm_y6LEG((6CjR;M81=i1tSVz!_n8|y5v1U^z)8S~4XyqaBLtlkCwg7#o zgy8jI4t|dq_G_yG%Cki9E-q`lpdu-!YjZ>c&H7L2kQDe4vCa_G9!QowHkPUx2B-Jg zol}}4t^VDy_(NcaL}Q2svfS_g!6j39f@z-HvGC0O^VV0bBr%($EIg9FMyM9P2oVTG zeQ(w_kfl?^GyDw^jSl@#7n#Rt0bLN?rvpy-PBWd&CS>CJT$B&-#Z0X;Yw{sbK3?SHY$>R;;DNL2M%|T}zOyM(FIebz z>NaiKL(KLgJKqL9r{HFj;kyTFYnx1u2hR&TsbP9BX#ppqpt`Q#kG$8OfF~Bf_E6AMKc;_k-LTM))xpnbo*oIT^e%^HS%b^Kes4mRw# z8-=ujVi{#T)Vhe#(Pl*Ibazsj92zVe+~2XQx5G%?BkSQk{Zlid@J6pqGDR;!#CimE znwVI%AQE?U1;Cs#mq@<>dJhE#t@9a9{Ke#9~w>gzfzWX*nlLp7sLzdkz@O!eC>6YeOU= z^>jm+;(_oQ6K*Jk(8fVF;@nft90n@~VczV*2y^1`|rFM%*28=1M& z7xY700sSx~z-(;(J%R`?K)h-`xN0AdM51r9HZC1?=9ihendK}zyipR0MF+=EGE#C* zg=^Nj*%v;f%lbPN?UKGnJWGpq9A{{b)%8pyu51R@cli3Gk$=KN+O}dZQ*g>{yo25i z#o=B&i&WN{dDJ>5EPUu%6pygEycUYGrtgo$XC+(MQ{mjU!W`bl3&x|U^t+}+XXDmT zl+JM{eGoUaM@*f$$#gNHVv2g9$C2#M|Ak?y2+H}29`$)cp%pA5b7GJi9UPvMunCSUM(cA zd1Lct4m>YsA*-|k3Q;I*YDkzmG7o=8=l@X6muMXD%(Tar>7Y>@R@EpYEekp$4wJQw zM)K=RsD^ksvbcyRzFD*6WaVw(Q$N7T^`MiZmsl)G^C2-%nSxz8EEin8#~j-l%8F1v z?OIHvwcxM33|VTE0Iq){nZ{cPoK8g*UxQS z@OjI^l?|yD)fQ@iFM%?b8G>hiIO+l+X%L`2aZkY7bOn-WiP_TfJ91ybHa5_1uNwvy zuWXQ;@QiUonBqRd?@mL}xy7jNv9J%wdq|5i7-0yq?Q)_0A9Ay12pVCs##&$B=?lN* zu%)~Ylj&BZk+wuSklzr&R~885ts*2pzi^OE2<3sik-b~D?ylPR!-%;r?;+lA@)I)m zfR*)x5X44AJ%{9sSx7qRkG7rxlNJ#N)HJ3d@+09Gz~-!br=J2G zcqm|#8qjmBfF`cO-CQI$?Dt$72%A5jQnvSoLdF=^^n zFE4_Pd56szZ{h#h2+Ak^9#59G$flS{5tre9ijFaZg`>DT)P8|^-w8XKuqWTIPyR=nO zjm`C>C1PyO5T0$xqZC> z=M^5wV6)Lo_n4-m*e@;c6tjy&^yGW;ZHV(vkyRSips8M}M3*G4^?P1V_}~$NOL!8B zLHMF&c08F<*-H?gTcH}-cSgvymJAMQFv>UV!=QW|iR#NGtNVHsuX}A{b)Zj>5TjV= zd$2Md!XjJ^q4KU2m7hbcu==)k8)82H^Ude?2MpUwu&1tNY4uXsLf!Gy&EFn~n$}`4 zk2Qg4yCb7>8wE~#iH@6)K8uQ^Z4*&PnHEWQ`$E<)B5y!a~#3Oo_d1S%Qm|+OBo*o$MP@MWqmd?z%RW;6b9tK0rB=^OEO zoA3Z9&Oksqg3!(G2p@#JWi8(#w!b0O|`HkfrTjK`Wj9|{eboUbbSY?y01aNb>E zZ*;~MU-&N&rL#f4&giQ7XidOdj?+w(n;MF4*}?5wOy#&*qEs+e-7yzfSu=LkM-OYP zx}d!eVZRociRUEquJ<~QI%`Pze);Q%mo6xS_G%zF?p84L3<2BOynw7eQDp=_RzSOn zXPpLeqDe?CZ-JXqL>5kLMz5{i-IAn|=;xpH^-2g#sV@FGDui4WkWDbb{iNx)?$g&L zO+BzaCc`mjhoJJ>1^Vvl73Djp9}z~0bICkh{o$*JptY6^1dMN9_En^Sd2ZK*)E$=Q!cQ*JiWfnryFVa+Kh#E|`Wkgoe2on@}>M z+fPonc+u8UUr2e;Z6gwi9*e-{v4Y5blH6y|4Z(_vcuWp{O6%0)Ia=f&BRizlL0e-G zt11#wi(@-X25@jP6&l$oyRu7CQt2S4v+=Xf#_Mv?`Z5FYv=H*CyrBIRycthgI0ca; zD$)&2MBV6*s)qU|oDM7CSoj!a*GW8J%fmbNKICtd4_Sebr80siq2V7ZapvpMJ$n)p zj^0umJcAS{#AytHTr^$(7!d_E@L6PW7R#j0fq;`t4ixg654_~c$ll<0#|rnvJ<0VF z?-#*SZ=)Sgf$ZZ(0F>(slAV1^g3@)IlY0pQb`(yXAJZbs2$(`$L!M-4^?^wIz?l5} zVEXKo7% zWg3fwVfvL;9p;q&Z;jKgV9=vcmRXq8=is>#3+$Xd{@5!e0cjNR<&d?dQpS^j3KWFd z-AK5)f32-#-V%+!qEY_m=r`YNj;ta5Cebvb09`<$zjIQmqtS-dtp)tAUGJB^hhp}N zvgVh5+va!M2?s1EC|oR-MGKZwv0DVe4)pEvEKk*AC;fT=06+jqL_t*aR@ztM$0S;T zR@iJ*w=hi`I|ah}Otk2GsFC|2Jc4FGn?pDWR#aD~u(YEe(f9;I-LpJ5wXUt}C9a!z zC0(aD@0KFxL`Pl>DdQ0Oxj)Jy6i@F!nqTCQ5Q3Xskh; z^0D_Q!v;`o@tg#Y{3V)fe3;-S-clbu1CFCvifI%}F=Z+VO(78l9aD2328MZyq=6}u zqsMP6-?xrvkwl37(nAE<%Lq*}Y$;jp@AHmevaoe5+u+pdo73Su6iNC!mOy)8z10k- zUbf5a-cE4Ly}oc4m1aIhm{&g7MyDz=;l!XOKRAYN#Cux`StFLMGTdSaR1^d;zeC;7_B3N;1M6}Df&xV# zv%9pyd&>C!`N6Fq^MMob8fZxCeW}iG3jyo5)&4)*XNmY!B7jKpN;_9x!G3GJ?9NsN zUCA6=aL6|g{Hy09Hy%}`wNo$elC=+g#S;AaLC#-P>iY`CUB<>_eIsfEo^wavyui=T z{Lg$+8i@9Zne+lcS8@1ZkfBLCL4uAsV1)7aW-tgYKxu)+;D?!s2|h_RjayIhI9~tH zwC>mMa>9l0?Tbq+QwL?9gH?lgdKDgTqdKxT@MB?N+vGQ;_0f#Y5k*R?*b%iLQw_!a z&Vc~_8{h_p(zI(rS)TQUSP%=4$l%e$(F!~{2rm*4)lh|+FkB#RRCg4FketrNxSASp zn|oaP)^H8g8J#aE0JRzoC5V)Vpy?UFTZy0{f?(XucXZ?!ehkNgR2e0M3{`!qcZRzR zzcRsdJ7}L@9nBfaLa@QBniTYn?~&7@6%)=OV#oz+4ePvz3j~xOkWDlGge@}L>;r;rY4VX0_Nou*%Z!X zI|u?>5SJ9)o$Yy(IE)ZxHo&h@0}++7ad4J<8yed(&cCVQ|AQxy^6y9v8e!F$MRC5l zh(SldWqsW=^Wm~wPjTWI5uwc6+bxMG#>jlP_?1^`n0)OKu9UXA_01t)7f^RD4{K@; zV!IcJHuZ`l>E6w!lQq`Xd}`}jzvnY@p9A4=s!~xAjHKrF7fquM0-*!osq+Cd-2^2b z6_6eBbQ*bavgSQKOh6_-+V-mo$xvS~Lw_I13&yjor5* zF2dS5a_p!sJ1aSUFAOzQNd3pOyQ!I=daq$1zjNSh9F%Tl-04bDev^l3|7AbY&B)^Tn zTUO>D=uX>@>OI%#f}Rqt=WlCkp{xM=6?$<2o?+yS+Hm)^NJ5i>!PRT(4g?9uGI0n( zqhDu@dp43}ipHjcmSk(XhoXa((<$3g?Ie97TI~V69uHn2=a7R~H;3O*jd%vNt5>f! z`cIis59+rctOls|O)3LBXy(So#%S+$$*T}fGzQrA-|5`g;1u2NhL^q@I4D*%$W7Im zmJ~XUe0A*qxrSs|&&*FffJNo)F@F4hcS_1C0<;W;;3W$Ui5KMnuj3jOE~O~I_fJmCIzMwiQ)^MFu5Awh zjj{TC;w?97vXYJnHt?7*Yoq>D4Ye4l7uHu<(~`M!7nJ(?eO6L0c?8QEO=(#3YtOvx zJN^2n2LiE6WW}5&W#nF8{@u4537_%Gl`AW$yuRDhTta)SuZv)f1rREl1R)Im4oV~V zDqyc9yM@8yKK?=0cayB%1#75Wuc7Xua*luQ@BQY4l#X9j7D!@5;XF=Aiou1_8N1IY zc%H*es=9DY-eJ;;)(s^B&P>m`3r-=>YnlMWxpX+jCenb9>z)+>{(O?gKq9|WkGXSXZuT$Xsg$KE2C53!z(sNQ;tRbV(BWIt9Ebp^9* zK0+0$I}RN-SaznJx4+U?)Fs2+5_$37^Uq890|pZZ+bF~rrXzY~#oNzV{Dp;-DTM_) zZ{x1PL}(9ON>k$Whi@lYnu$CTV?q|pMbqv+$!;=dpcrN+#pFuja&Xc7S6->P{`!mB zG=mWZVPx8D;nJr1lErVl5hVSoiz$;aq$45L-}45%-=d;)OB?r#!Jn~ZIuEn6F-gM*3+h6T=s$_NPYHF_zW=y2MTyMoPLI}i=hEJ+pp`*6j#}0C!M5}_-d;V z-@8m>S%kPT9RT;BDmAmvG?gcRn3Gk*^{Ee8D9bP_!}O9_rbAU&_YEI{d<=`TpN$oV zcn|K~-7_~=+wXL89nw<}K|*J%x==~xLSh{Ltgo={sC^C9UW&FRp(0lo6h8URK;`P) z@yNl83flcFRP#VkZiiIaQ!xR`qGsLV{@K|_J;%g2^zYDfd_XbpcM6Ofpl>B>sDHn> zo3nm7vc7Ci_5~;%cc2*Oa5z$jYLs@qPT9IPMq-;fxNVUciHo#)CBO43@|E6@8hh*c z!)vA0*T;Rk1qm@|>zjZTdcs0paoRTv_M0nLnt6G7MqZLAfu#-HILzg+X1{=D`B!A3 zLXn~zz=K(&THjK+W5X75?y)x@>tL6dc2}dAYC*BZV)W-6!@{qrSArCogU6nKBL4o} zumces8*BZOkqVlr8p>`EvF?k9=+kH+zPw^p+M%|*tUJBIjxxN#pbqJ;(>Hd6p|2? zu)lFlQdL{+X?<;Cigj^!%!nAWLuTC&gy6oyQdq>k`d#Ey!tR*5?M_IU`wY5lwP z*!_jh?wf(Z-mhpHf(WSDF1t__iK>?%jo@1Bz(^6Mmzq>;(%|%TZ{oMaTWfI0k~64G zXmU=&RM*idu01_z8s&&Eh5m(OXG82dhBKexD0+iwYBOV{2euGRm0SW$PEWx32W^*r zJ=jn&lTdLI$C=2APw*7Kj*WOD|AiT#1 z^djVDAy4a4+@h9~aJ06XxVWS)jS(!@ActsCnJQ(Yc<6f`*1R*hOU4&FcT`-IVzu55 zQr_>=L(&)D4h4=_TeAs#Rfs+&Ei$_LX}$LupYIqHwS2YKdS*VI_^b-P&3 z#n|+iMH$d3Ig}W)O$`CS59}jR-56!%m^d;yP>$5JB3p3jfrhdhC-&|g4Fm#76$VE% zXaPlDzXf4X%VvM<{r+r!6B^0t>QYhOv)M<&&q({pm?g$1(I3#d<}{hwVz5wofRCTM ztsrg3^j6w2Id5gT|D4LW`eKsEc0x9s8AO%nNX72gCl?k<3(FdB#~waY(kX{z81oz~ z`vbxb`#^J?fB^SZnQq4|BSB2w`o5%+`F*3$JPXBi`{|UnXh2%h4&3Hv0x{9X#A``Y zq({u4KVTTDFP>NouIKSsgDbs?or4 zy4bKO?`OL&aUhUr2V(vl+hFowiAhY%93r&O>DAtUQ9)+IrZ=CeJcvB)cG=0_zdmis zuL@prU1Ex@{^;eJaY!;C!RQsOM@>_8F z{2h*{Y~a7M6712 zt=a)BvdvWN*7rO1Jy}>yQRNF<>kX_lIIXxDj@)kD2^@4VAdQCld@V4qgD4m@8-lkv zR#$<52ejVZptUC$OVY%hP4S$MH!TZ01!+Z~GV-lhwzmqERu;s9`WG=>$v`@46(SBA zp|NzVRn)7jy3n^deD2uB;O~tez0nxbs5dyIT(q-UMxs1aWs|AyQ4V^OlST1wAU0W# zTFfsQOz1_JiQ8ZoJ}ifY_5qkM5-q==o9er=PEVJ={tCbMV~2Y8Zq{&@fwQ)( zL0Tfp#*+H2TU#{mHoEht`)EKi9Xh{Petkmlk`BCATz%S{BJZ6fsUp!Kkb&2@1^x-d zIyhvSn$^F&X)wn4a3lyer>_n+-d-asvz!(-pGRya6r@>A{-zI!;3e8(K+?|e%Z9@* zYwO`&xwoW5dq2Vo_k$w#acrQYy}{@cpB+5tO5BgU-Z64)3LIg*bzNHnL$$+?Wp(ZG zF-;v+ODzo37>PTV49%gAYr>K8Jvp4?L*P=~kmN9%)nkj{ zls^f=>3p7x|E*u@Pt|7=yHT*Jo1 z(;FI^U`7)sTeH?kgc7jXqnN)E*ZfyrNgsz@cW0-7*H=HGe|`uTZ&NEJxNREi?2ZlS zB0yuq;p>CR!<4`g_kN6(SCb@#5>SW68bZUMrRQM>w@5~OnX2>2vJ%=es9igveYISh zw0~1|Ll={`1_!3ui3a#H4S{G9*9|^(v|C_HTpAP7d9j_FSM-2kOhCHwP)yVgNOjKC zboGYy6(wJi4Tkg`lwVyr3$+C1q62WrZm34t7`3&!s?NlOEhp_jMxu4Xuv}^G&YG?s zhcKv}i`wgtvRB4Tbx9~#yP9Z_OZ|!Vs@b zsYw3{(2TaEA~3Nm9Gm4}ts^UaRaJd5+hs*){ferHd|C!i-+Z7)yPeaI^!tmFx}K;4 zeQkzCoW~eSy2>(fLuDLD?5J?S#~W2u1Yxr%*1^ivby8gqwMuEs$q~>;+9J#P(m8YF zW1~ArI}`Jzt$zIf^4TZ5>oX+qNF&tllw6T{tsyV1x#Bl@#`C#TEMIP`K>V#JnRyyTh=vl46XK=Hi5l02NALq}#^{j01cEo3eSM@q<~j z!mXbZS$`9!-3woYnS!hd%uRE+PBEtePUwTh;=n)1U@8v&7BU|dzy`VC0$C!L?gb~- z&qL=(^b|+g&xRI1EGnB{VRJbPR$&xWu*GnW>qa2*ZEi*iN0KrAsONt|gm0p88&D6e zVrQ{2p|D33gMt4aeUM3>5QN)cLV%v@Q4n8{6w?s!m>Pql9^tCXe;`$5AMe+XYrRt~ zMFB@>OA|nlbE92gY{)_hA~b0}%1Knws(5Qn;po;M&J6pd?-S=Iq9Nj; z1{;iOR(87O27hIUB!rN2h&E~w>=Q*!+#$(<^4i_I>l@P3nF*~njl{LcE2#}<75E9L zm^>f0)DL^p)62(`$#&%Up*DCjGLr_nMf%5bU(KH4dGnfa;_*|b^-&rAcJv$6m~^#3 z#m9iLzl!{fv@yN+pw|(Sb`?Xhk5F3pGnEl11GAe74S4})IB)5ZMl~-aay}98=XM<_ zW}#XoB5p4Zhp7#_dnE6f-o|D?&QBa~?Wpboc9x6qOzx6(^?qp2{{##DLX@=Gm+8>% zHYA(o)s#0d;k$TLdhCV0GwSCZYfnZ`$Uo`z0u&hbps|t45k@!a@>y) z!0$CO(eJN7HIc-#C9ez1eAAIT{(w)CqnTFjjP>nPw~;xqyuv>sq-pcCj0&6 z^E`(R8Km4+LI{)RncTf!E+pk>aD~;vTRlQB%x^=wUI~8f@^uY?KD*~1EK0;36U)~E zSW)nLHLYVuWIY>>Jf~OyrR@|%s*ZslwI33Z+?Y#5_Mm31G2iO1yS%j=Nwj@Qc@s** zqoA>>1Z@I0&B~e6O^q3j45f93SFTFSbqof)kspouKKi2^hbL6{COSqSf|R7B4s3a9 zX@Y2m6etwj;+<9t)eo#dYb)aN$99WVPp9>hkr9vv@A^)yWaqx2Lx;w<`@_S*qmo0b zp@j(oXY!!pIlVI#&RA|`xjf)^e=+U){2yn}_7V>z-K|%@K%Dv;+AtbszG3C)MKC>{ zl_ct9S}-4tHM2Z1Dt30mCGa4X*1>UO7#@LFVw%<@>1KTrOWhAA9lYvnZx_wfH>zr# zjbVL;Ec;nrK&G58P7(r$0RBDN68Wtcm4J)sSadZ=n_KVji7{;bx~(DdGJc{WwQ;Vl z>es+q|4AUlwXN_-OzL99CEMg!fx-y|s-!l6qH)Xkf=nMGA~+?Zvop!TjzggNUm=4& z*%J#qJptO^-aGHR+|ZB`Oqts?gPC}8n!R>2@Mpo`ze3PP-?%CdCsl~i4ll+c@21>6oD6j<1gy#Pv;`)p)!_6O z^bMp>Dnib&*w|?t>ZlU`qScxv(WwvH{#V^6VaRJRFvJv7;tyHYWgSG{S@s zwwg+;)~~ZRfu@E!tvnbxtCl4;ykncU!xw!Hj~{$ZTwPPh`!?XJ|6082v2FV!{$6#k zp#ZG>*I}crM;6YUT#@du*L!GhLJ*RBk~Jo$7r5jkM+YM!WpbibQKg6vX|~m+%;A{q zB}ialbvAZ{!jpoJpf6*p-$Hu@9V7jcoeDibpudF>zEN3w+a)mHMQY*?A>pHGak>9i z!_fO$ME2n^SsoHTX%dYVQAf5EQ-Br2o((dm4B+zL)5y=~tsJ1GhSh0F={bFp9eZ&8 z&#XSuX8xp;tdS_?C`h(<#4{ZRQ1C1o4mMuZt}j3-c+xj=tg5X|MK!Q<>Ln$OtS4km zb{8q`F(f73ZVl@*lr+N>VvK_qg@K3>9t`bf`KWBq4}a9={D%l%WDT7SEapip;zf!g z&jxvT_4bCyRD(8e#TbOKob#ZK6-%c2tc?+O$UZH;ux3kuEM{VopCSp;4D;l03IESRFXEe}4ubE24G&B?lQUakO9~eLVKqhN6@P$IVBz zkLRMQv{ewqxnTF1V;Yf<5|uZ|1SZvn5h~RKX_P$#jkWWlyq4aZxsRCj%}xlb>m<(P zOpVn^Md@)!!aD=8pEiiD%ZgJE6U~tLVU@P60!-1}WR$`4XU!@;_m+Fk;bf^{U%TW@ zt+nlA+j;EYKU4=#^UBHqSSOVsRX@!ma03zaJ!xHD+Rh&H=FwcQVMz+lWkK+=aKzPq z5vcONi8^>>3`u{6-gHw3f0)^V5VEIh4F3xx8H3z|JZBPyM*8cm;%GYpehFU)oCUNW z8j1WG&^l~B4uamrV9ZY9sCqC*s_b}e1BJ#lX9 zwzhsx=0O|Ygd5bF=Zv**~S{+jg?L7CK+ro@NWR~uY#Fk#u zGiS&0L*8^zu#7?yNh%Pm3M5QzI~7@Nt?zLn-=A>dOM>v}4qYF_AooGq@DK#o?Ui+N z77+BSOW4?LClF_(fg7J7o+~gUP*% zo5I&ZgLx4Gb49gZo(%0KysIht7I5iNP*Uf6y@4lDo#d!6xwhI&K3iKWwJ&K36xu{4 z0}z7(e0$|VI4@gkT4ceIa}$f|#qD*4xh{H$Ky%ed;Vwkr^Z;4Wuinu)tp)QX=l`n* z8S9;m;Y;8wJOjpp6U^a{!=&t`(B=cI&Y#!o@K_$D;s2}m_+LMt;JT@qVtdKW)Jh?9 zG&IaJ;Aq+or&4L$2yedNS2TaJuDz-Gk4d_AGlB0=UG=QMX98>(~t=dL2JG>3HLhgsFBAu;)vvz69p#HK5BJ0IJA`Ts(b} zCaa5NPEvQ2l|@Du^nt=b52yt$mMPH75WOE*{oZ>@YvFyod;GhfLnfZsgMizz6qps& z=qZ@&*8@M)5DM}NTpHahJiDbbNvuqZHGKhQnyVpX!zi}6N0QXNMDT8bNdpD67aTw9 zfWOQ(6nPOK2@gvHJitRnZL%bA$}$KELGqh9Xccg~r3Y(riN~_CvQmtjbP66Y3pzNj z?us{KqTVLuOI6Ze20M|LXx7;_F&*T$*cU*jr!j%Ch>EZZ=0`kDzm?}kQUX#`QPb*G zS&u#8sEyA-n#=)0Y$`IsmxhI%WTCYQ0o6DJtyw}Sm9E4nRNOW9J6Ps)IJ|B`8tds% z#UP0e&oegX*TX9PQx@#o2LkfL2hl=T)HI#`O=;7UUtN9MYm3VLvsP6GCNHn`x5pVR z&x7pZgrErAJr{!&Y%ZFxmrQh%w!C=vYfqNLy6ChOWnzdng5MD>nj(ZB9oxIt_$&5) z2X5Mh;JdZTn)R%qx!t=JlL{i}e=ORxLQ>T$5VIS|33x6Q;W;{mCSu4j<w4V%$clpy(}9j}zvo3XF3$;4?v9*&q~dmZqe076oSCkf36q8V0>f(i@SEeAsfU zDGhDK`2Gl7-@rP$AWk)7CCO8-d37I0oZX%sxm#jX)4dp{H?y_qx2c_{Rr z2CU%2f(%E?Vj%q`lP`bF7)O23>@EkGy%+R7T>hGTM>LX)uDQb|j zWr6WC7rOL=k+}b5LefJfJIc{~jGq88pe~)aYt!lrdki?A;>~_?$tMMSqg2kl5C&_Z?OX?*p63yF znh7e=t|m)av86<)h<-2c{YU+MLWM7h#S1u7pM+X@DMC-M$l%yRrqFRu)|i$yshqM9=WtMW>MpO)PB z*n?<8tR-auoK>UImMLL6@()tpe9^874@u$x`wHm3@G~`2U6x`IQ0AAu4iA4a1oe8_B1{^K z+;cM7e{3|JOr$P1YOswCn|LQo|I-1FT*m6uy-3_ZE`(XZanx`K#(S|5bOPCo5Yv=( z^1fXqM9U@D_^haiKc3Tx2#p4-kBQKw;ZycXBjQ!LKHcb=PBJaPJTr_vPt6d6LN zL(_RwQ^U^?{C%r8cquP%FF>=dRCML0KEE+Q{@AfS5r0GL&l5C$SbaqO#^VxhgDHKf zjh4?eXz_By8lEZZ6euU@4#@QS1Z}R5$=cZ{j~9pVy&R#U7yClVVVBiE!ojiiJG@of zk2g*yqOJdn*H}~4I5=W5m!O!N6{W2nu?hSPM2?^W1Wnh;+C3db=R+g&eoBy7UtE=p zfc#IwvNkfVnxCb(n3)%~PcMaOpWP@M{{S2@)Efo%Akf2$yJT)|Eqq(|pEy)yjrqi+ zr~wGU^dsh9^oaaUSvDS$I;E^mtoOukNIY)uPMz9;cz6I;z3;1IL&sp*bn>d|5crtt zRLT2;%b9s4u6q&7#wOLrEH|OrEm+&=6_80Rv+etZX#-EsjcZcWVYi(OS=56y)rE!@ zt!#+JjQ`{uXfbmW9sND8jW*9USg5DKi#P+^3{wnF-A%$oNU3(-dvGnFh-*>4WDcSc z#sSCdAc>kUF_2c#bt)@ZZdZ9G#p{nR*Ex1=Owm$%LKY|5gtTFl2d6lszV3-|>i40z zX$uwgK&+8Z2^JGcN z8TH@bhD(8VZR&`Kph7v#=aVE3LOX{un}xfRr&Hiu1vE;9!6zK_9O`cn6C0&|YQQ9} zNhXy`sV4YRnZcPhAqkVABqiDQPb7jAubeZh$k6HcA^3aV#bNVAl;+up`b5~pqC-s;wj|5{g!_IC(cqjESR{b(z_ z?D4g>JcjnjX>u{oVV`e%1T}5l90_))}z$b%;DDJ)Ynt1WJgm zc^vb%52Bu8Anmx_Wnu4Wim6>fhCWx_TX!?KLmaL9Pu9|smtLwcw7{Z-ydd8rJI}SY zDsT}pg&uGV^Z~!34iA~hcgiv2^SG|>_QxzY+gW;($e_ThBCn5E#g`Bs2!dhStm0YV zpWy(i&G)Wfb%?+W|LHk!!h|mw2n;=(8q|lf9Q>TAIj~}Iatm1smDIH>>iF>(^JX5V_#;{z7J4?6SI)-J_0WJN z0_smZqj!`Pv5QlzRYZ+?V9Sj`kgA{^KNG%kmS({oMJMp)Hy z${WMo5zovKT*<|T_kh!3l8rW>Y^bX{D${BTpo5~KB6J7zaMq=8`(tx)m1g$wM5>`UX zjs6e4nU1*gjUT;Ms#;`FxYFA}np2~5{5^pFcBZq2f7BW6djg_r&UQ--3w>z!4lBi8 z2TN%scJ;2XFwVoA9ssD&f+DHAQmx$I93`0qCj8k!EUh}mxC`96VK7gGa;imM_@le z4!|&tG5a}a;Uk1m4#Qe&Yzjr5xTt$hQ~B!m6qJ@42S<7eLW6dJ-EPzB_g<jr1cI(`Va`q$tqDfHlJKJ>#PzCih4NPh4{MpN&uRT$X2g^@%GX1TM<_cLpb+BJ`uoIt z&EV3&?@LI6Sl(m@uK=arvdX|;;FOyzgEs)>FFsCo3jq{vSO5);ILMj7e8z!Ol?aNQs)KKvyOw4?DYi;=< zK`=KXx`OW5p1TCuf1F{_c3W8L6gU>jHIa=XuJiBh+$n>8N%JH^h-4NL4Zy@i$KEvIe~uKa{T|Q;yVoDNQAusLsLJ40|LwH1zuDaoobWPe zMakGq#ZClfsL9{|mkuJ-2rVR1 zYd_aj>Ja#oISs@)wVFA@X>#L-U3crb#OzOu34ycR)w{NYC3A{LV6PMy{#C{_-h;05 zAW8sVp~o8#tNl{~P~vyZuf0ZI3hU%1%zqcG-+4T3ym}?7cTee`6?0jH-6&B62%ZWd zW9~<2p+~LnL$G+Z)d%{J>x{a3n#-?h_cA*52>|9KRcA+$azAfvS#=2U{Bu->dKA8l zR69-IrpIe{PX^~Oslztt>RZn<4F<$lhP=D(V9}YF52XJ`wedgr#|fKk0tG)ZLpg1f zSwLlHw>+#wUvz~7XEQWcTIaSG6;&q6Pm>)Eyarbw=CsL#KdQW@sjk#lxoH&gu@gc3 zUzYi%RQcpjd79~i*gICz%#loqujeY{{F4RrD$EoJHIRFQI0dD4BuL=IeOtdm??p-> z1r8AgJ7_!zbLO}vIr$wt{3#Be<1t|xxv(b9m_Gey+BCU82skn!=#U+Wd^?7_6X(B# z6Q?3pu^dG<@3o5ZoY84%QSbUChuoYw4VuCVXoKxkoq3qz>9>*7KFsMXq>tT!m(BRSVv{adf#QB>Rx>9tSumv58!^eM8mz(?y)aCC~tp7Ifr65hoEy9rwFUQVHB ziztj)+!*;gf}kIU#gfLd>__;1BN9{|KW#|%HH#N7ZWh23z!?lDBuK~%He6QLELe#a zt1-4aK*Tg3#ZEKvxV`|%IkH8KN1$CNllyO}58nYo=+WQ->8%?2E70)UV@w?|6B{@| zg9=UCSx?$Yb#h=i;F9m*fW3#~Sifj8SF=+5{fCz>E!$eTf8&>LJhvy<=u7*y%HNwf zaLB)28F}gcM)|B6pA|LFQ(2ecqgDH4jUz!S|2Gff-g+G(&MWoLV)bV|uNnK+6PBjMs1KcI>|Few&7Ag+dkZIngiCun}Zec^s@$4qPb zr`S2Kw=lFFCjM}TV7Uze_3dXK3Ed(KZaW@;r0Xa13}-=#u^wVMLa_WZVVbxnc?Cj% zinh?iOePRBLB7=nltIqJ!p{Kb1Oa_ltCDo9s!&&hg5*No!e3$rs@@)%nMr8vOd?#1 zy=7ZLM1B+MnmnPJMwF$Eb4-(awc8N|D*@0Y4 zeE-ucXh94Ll)?shZ0YZ!VjR&m#)b%}h&h_S1C@i+!S^=(Eq>0)YgPyZ&+sayN`);RUx4j-4YC2$! zonFOwzcJ){4s&1^HkaG;2M$aj*CP`gVNDIVhFh?v0>YtbVim#cd>>yQM93TOj~HNK zqbsnvM(a^^b{^;`;iS9>zfms^ zN`l|cYtfGgfy%r&uO1?0oX7$r*^e=SJIhMZU3WPLAf#YBJA8`wh+<_0NO# z7na22Q&A2h+hgtD#@MzVmH)rrlkp`4LTTL@4J2#Z`NoDNPe23eJYPSyq0c zkz~#vk1W^3=ZROh#i1ba3A&1i0^8G#z@}BZh;mluE8P^3)MYV+di{;!ntoKt*0QS7 z@HZs1j{KHXu;vB&%rMB!>|X#%1J?gYU787rAp=x~eB>xhDy$;tLrzKHe)s!2S8#oQe6kRR{Sa!#yH zUL>&5znMN|YJ}1+<$3ySMUKw)dpr?JlKiM1`5_kg<8W?02@QHV%;mnwMo0lWSo30H zy7cYkWlfkU^yBb)x)Ix$DyY=o-rv_SoP?Pj`v8c5AV=K%f9+iha8>17+jTqb>}<8{>~x21WmU9l?bHr+x>I#$ zp~~tCLTn^N9wAa8ypxbz^0-NEa^L5D_V-V`at)+lCmq-}X9jL^bM86ke&;*?@BjGy ze?O-N2Azc!DIP+z;@JJ@cM4|!I|d)IE24X*)0VX}hhE-#yb*=8wZ-1C0$*ZCVmn#RuYTPuJHI z73C*90=kA$RuBdW=@*e37}No>zO|?*nJ|)1DGGQCke{_p9%{l2YRm2}$O(n8KmkiF zP8E3h3B@a}`&=~jFd6gkT8+??+L{HeP_h#ft%63;?0>&v>s?aQV5& zu53Xg&h6&FKzMFtFm_8g+(i&1THx|SN%GRp4rlp+tfo~IDQhy8_ujOwuZ}}m1m4q< zA`hEd;q!;QlB^`fQzPCwUEiPbMutmv30L2$K>dn|Y zDe`Q6`9-on*TpuWP%|p(loyn(h0Xge9=ut|4`vZMBA3tm;X%D_=+dU;m7Z=u(iCp` zbZ!REBNc(+{Aih zb2%xN`UuEzBniyu{Ifl=&ixdgQOcz>P z^FiKkIC2om%t4& zyUVoLV+M~qo_92iOs%{|XZ<`4&ZKn7ji-YUa5esF#zq=d6Mz4Wn z=i21}b7OtIyiyK)(E|@pD=^*EXF(B)XhQPKGJhqanOz-&v(ZTLLxh3{K*3rKfzk~q z$OW(#zmznz@7#YCU6Rc`$zogL<}k;(O_IeJp0Oe<_B=#Z+g)C3xUuMB+b=l|T8s&K zxNsir?GGXS2Srr7Q7{rm#(Q?Ir-B@ZNAW}8ER$#e6RLGT5F=a|vNscuZ^ts$dljzQ`eNAaeDB=uo+{c4Jus7wR8>M4Kc=VBImd? zZJR2L@9rQcZef|oKJs{T=cj$qg1Ont#J?HT&CP(qJhuS_0{T0cnhI=IxEGgjBl2O7 zDQe-?KxeZ)R6M;_R9JO7Yb?Y1^(&)x%BdXw@hl1!Mt{FIo0PH;tt52B$o12>gP{WU_M(7)VB_It=^Irux7z~WB4o7ZMW5$eNJHF@#k&!m zb^nSs)PPrXJZeZi7e98a2j6W3?ncKvGMIco1OYwx1R72ZbR8Zq*GT)47uE*a(W!v| zKf?WBgjjVcAHYhMx*gtfz5_t}Pi_}SfaWg!Mq{+UD_SEeN;T3x=?{BOA6iqr*jp3k zDc$giXzU%#y;RKnN5kbG@-YFVgPaGIv_qf&YD-M+0j;5otH$yp_-f!p`{wyNC^g!yu zhrerX(t(rkwz{O4u*C zeLRz7;8s0=YXuZXlnlX1qzzLK%5se$as4*1vIsj_LU)D>;KjHS5A`$$UA@xDNUY=6 zzl&QockZisg+WyHjO2CD{6388oL_Lgt|kXcd}0K`m_(@uqz%u%-4Xi>h&KKuD*0vT z>>R}(x(`_cJmU$8v=C?s(YCk<`@Ft;aQzkid`_>9Qdm*8DLnzb!B zf86Iw`a2qovNB1N{2PE>8;T~gr{8Nk+OZX0JcVFBrkL>f7JvdT40K!jsm9;@wyv(u z7zyW*-`oU5#wMKezDK_AAGC%)XAN092xm>#K^}6+%9Wl#XQ!@}&s^pa9prYT59e45rs)dG{q$((1h` zw?)x~Atz&|`~7m@Y5S9Aa7c$!AJF;o9Np`dTpP5lfyK#$$c_xJIc3I#2#a_s|P@ z$YtP1ZjK~sER|nrCGFju%6|otRqqFwycjxgX;fYIfWO1)uws!1#)&NQkNch{Jh4mCif`mE_o-NOYlVv-yk( z6=?8?#S```!~&iK3l#DXh7!gA z00w+XL_t)pI_HQ*>8$VlTl){01NX9HZkGh(fWeq_=0r*-0{7fl zAAoW-&v}-+rrQf%&sN)~TYk=u21m!16I_jTA{n9FeV)iy;t|m_wSXmt zQfa?3lL_(~q?k@PhUMj%*pB)_hnnT$h{i^Un&|m(q`<;R#{N-25`qEQzQ6HjWyc9V zN|^9XYwG?Q#9HNOhTR7eEwIQ6vt84!02$Mnp_Cm~y;5V=&bFJ!j%CS+wsZ}Yn~L%! z81gfP$-Z65f+^ocnhOn{TrVP~`AE?<_9vs;$3A-#IR+DXtZQO?cOcXjbia)-TqyKt zW|zk!E2jpW(wVh<+tF6LzqEw=vvMX8XUv5@G(wEj%{i_DMoIxK#(Pd$nla5c;_W2Q zt5RjacGfK{0}1~4;mkx1+IYvx8R`6jPi|jIeJs825kHt5fQ&E}gUNP2ykrFkj6Rg~ z55!aQu`e$O5vQ!XuP443ld9Uaxgk?9&a<<}+okI2fZtd2FpM;s`GkJVI3ChFfHy<` zS#nRf7GEjJu8!G0fYv&+i!u!ms!&$r{ukc*WkK(`Yok{xv^jz_PfeuWpFau#au4>z zmt-Ahts}_?9Zj3%_efSkcl}Io_V@1(AL8b+y-8@EZb58o_%jTxw(zdCg&?_rWG>ZLsHg!`Os__Uli@=SS?-gT9A5v z2LmVtqC)N{WDqAT!k+oo zlG6)v_1M}x9lz`SzFiaJd%X|JAM*NW($ym+(?5um4019KDD0Y$kIb+BQlV@{+$T1p zv6=gb){f+f%DmnRGYJT;Z7~=^1O!GZ!&o+QJdq7mqcnw0E-SN@rY2rrzTAbrywJRJ z=T)&Y_XYf(}hKeJP@gzF7~SUwCEqt6FK@%?DUCR(n(HXM&dX|$JDnaDGAc{952-3slV%ap3>g$g#fpHG>4a|*XSZ@vdp|eV9IMg!U;k%A}H7MPwR3o0Hg0(99e3`MD!E7Zh}2oJLY~yU)df zZSTC6*|>4z$EYD%d}~&$|2n!ztANkm3M7)~VPQ+YYom!yUI&U^E_oCE;O8X|Bo~_J?K)vU z(mCm1bPoFY`Np#)#g+aAHHPoEi)#HN*yXD|*?~yn2Ba)huWt0kyiSUmM6UL;#E3Q3 zE6)c0X}||IaLxn**)di6@{sLF^rlRECKjsGgNjg&26Wn{c6XaQlS!8rHM(|68_~l_ zKj)7bEjn{i#UGn089mD6|DR?AD9ylh1V%p1iZnGD`CjXI$2r`aSctz25G~b$mT!sKQW?_~vB_U{|_qYILD} zR;^lv`pU(;`ISDG`#a^RQCzcW!O6WxufNZ+@;^WM^QRjDTD-UouL|y~qjL10?$ej< zP4_iaRK&)@JulsJ^c{+_ND#H@g%`5_-#p#1-)YkLjvbN7-zP_4as(!11ja@g6LQc$ bqJ#V&hNLka<~^*#00000NkvXXu0mjfWe(}z literal 0 HcmV?d00001 diff --git a/guzzle/guzzle/docs/_static/prettify.css b/guzzle/guzzle/docs/_static/prettify.css new file mode 100644 index 000000000..4d410b12e --- /dev/null +++ b/guzzle/guzzle/docs/_static/prettify.css @@ -0,0 +1,41 @@ +.com { + color: #93A1A1; +} +.lit { + color: #195F91; +} +.pun, .opn, .clo { + color: #93A1A1; +} +.fun { + color: #DC322F; +} +.str, .atv { + color: #DD1144; +} +.kwd, .linenums .tag { + color: #1E347B; +} +.typ, .atn, .dec, .var { + color: teal; +} +.pln { + color: #48484C; +} +.prettyprint { + background-color: #F7F7F9; + border: 1px solid #E1E1E8; + padding: 8px; +} +.prettyprint.linenums { + box-shadow: 40px 0 0 #FBFBFC inset, 41px 0 0 #ECECF0 inset; +} +ol.linenums { + margin: 0 0 0 33px; +} +ol.linenums li { + color: #BEBEC5; + line-height: 18px; + padding-left: 12px; + text-shadow: 0 1px 0 #FFFFFF; +} diff --git a/guzzle/guzzle/docs/_static/prettify.js b/guzzle/guzzle/docs/_static/prettify.js new file mode 100644 index 000000000..eef5ad7e6 --- /dev/null +++ b/guzzle/guzzle/docs/_static/prettify.js @@ -0,0 +1,28 @@ +var q=null;window.PR_SHOULD_USE_CONTINUATION=!0; +(function(){function L(a){function m(a){var f=a.charCodeAt(0);if(f!==92)return f;var b=a.charAt(1);return(f=r[b])?f:"0"<=b&&b<="7"?parseInt(a.substring(1),8):b==="u"||b==="x"?parseInt(a.substring(2),16):a.charCodeAt(1)}function e(a){if(a<32)return(a<16?"\\x0":"\\x")+a.toString(16);a=String.fromCharCode(a);if(a==="\\"||a==="-"||a==="["||a==="]")a="\\"+a;return a}function h(a){for(var f=a.substring(1,a.length-1).match(/\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\[0-3][0-7]{0,2}|\\[0-7]{1,2}|\\[\S\s]|[^\\]/g),a= +[],b=[],o=f[0]==="^",c=o?1:0,i=f.length;c122||(d<65||j>90||b.push([Math.max(65,j)|32,Math.min(d,90)|32]),d<97||j>122||b.push([Math.max(97,j)&-33,Math.min(d,122)&-33]))}}b.sort(function(a,f){return a[0]-f[0]||f[1]-a[1]});f=[];j=[NaN,NaN];for(c=0;ci[0]&&(i[1]+1>i[0]&&b.push("-"),b.push(e(i[1])));b.push("]");return b.join("")}function y(a){for(var f=a.source.match(/\[(?:[^\\\]]|\\[\S\s])*]|\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\\d+|\\[^\dux]|\(\?[!:=]|[()^]|[^()[\\^]+/g),b=f.length,d=[],c=0,i=0;c=2&&a==="["?f[c]=h(j):a!=="\\"&&(f[c]=j.replace(/[A-Za-z]/g,function(a){a=a.charCodeAt(0);return"["+String.fromCharCode(a&-33,a|32)+"]"}));return f.join("")}for(var t=0,s=!1,l=!1,p=0,d=a.length;p=5&&"lang-"===b.substring(0,5))&&!(o&&typeof o[1]==="string"))c=!1,b="src";c||(r[f]=b)}i=d;d+=f.length;if(c){c=o[1];var j=f.indexOf(c),k=j+c.length;o[2]&&(k=f.length-o[2].length,j=k-c.length);b=b.substring(5);B(l+i,f.substring(0,j),e,p);B(l+i+j,c,C(b,c),p);B(l+i+k,f.substring(k),e,p)}else p.push(l+i,b)}a.e=p}var h={},y;(function(){for(var e=a.concat(m), +l=[],p={},d=0,g=e.length;d=0;)h[n.charAt(k)]=r;r=r[1];n=""+r;p.hasOwnProperty(n)||(l.push(r),p[n]=q)}l.push(/[\S\s]/);y=L(l)})();var t=m.length;return e}function u(a){var m=[],e=[];a.tripleQuotedStrings?m.push(["str",/^(?:'''(?:[^'\\]|\\[\S\s]|''?(?=[^']))*(?:'''|$)|"""(?:[^"\\]|\\[\S\s]|""?(?=[^"]))*(?:"""|$)|'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$))/,q,"'\""]):a.multiLineStrings?m.push(["str",/^(?:'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$)|`(?:[^\\`]|\\[\S\s])*(?:`|$))/, +q,"'\"`"]):m.push(["str",/^(?:'(?:[^\n\r'\\]|\\.)*(?:'|$)|"(?:[^\n\r"\\]|\\.)*(?:"|$))/,q,"\"'"]);a.verbatimStrings&&e.push(["str",/^@"(?:[^"]|"")*(?:"|$)/,q]);var h=a.hashComments;h&&(a.cStyleComments?(h>1?m.push(["com",/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,q,"#"]):m.push(["com",/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\n\r]*)/,q,"#"]),e.push(["str",/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,q])):m.push(["com",/^#[^\n\r]*/, +q,"#"]));a.cStyleComments&&(e.push(["com",/^\/\/[^\n\r]*/,q]),e.push(["com",/^\/\*[\S\s]*?(?:\*\/|$)/,q]));a.regexLiterals&&e.push(["lang-regex",/^(?:^^\.?|[!+-]|!=|!==|#|%|%=|&|&&|&&=|&=|\(|\*|\*=|\+=|,|-=|->|\/|\/=|:|::|;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|[?@[^]|\^=|\^\^|\^\^=|{|\||\|=|\|\||\|\|=|~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\s*(\/(?=[^*/])(?:[^/[\\]|\\[\S\s]|\[(?:[^\\\]]|\\[\S\s])*(?:]|$))+\/)/]);(h=a.types)&&e.push(["typ",h]);a=(""+a.keywords).replace(/^ | $/g, +"");a.length&&e.push(["kwd",RegExp("^(?:"+a.replace(/[\s,]+/g,"|")+")\\b"),q]);m.push(["pln",/^\s+/,q," \r\n\t\xa0"]);e.push(["lit",/^@[$_a-z][\w$@]*/i,q],["typ",/^(?:[@_]?[A-Z]+[a-z][\w$@]*|\w+_t\b)/,q],["pln",/^[$_a-z][\w$@]*/i,q],["lit",/^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i,q,"0123456789"],["pln",/^\\[\S\s]?/,q],["pun",/^.[^\s\w"-$'./@\\`]*/,q]);return x(m,e)}function D(a,m){function e(a){switch(a.nodeType){case 1:if(k.test(a.className))break;if("BR"===a.nodeName)h(a), +a.parentNode&&a.parentNode.removeChild(a);else for(a=a.firstChild;a;a=a.nextSibling)e(a);break;case 3:case 4:if(p){var b=a.nodeValue,d=b.match(t);if(d){var c=b.substring(0,d.index);a.nodeValue=c;(b=b.substring(d.index+d[0].length))&&a.parentNode.insertBefore(s.createTextNode(b),a.nextSibling);h(a);c||a.parentNode.removeChild(a)}}}}function h(a){function b(a,d){var e=d?a.cloneNode(!1):a,f=a.parentNode;if(f){var f=b(f,1),g=a.nextSibling;f.appendChild(e);for(var h=g;h;h=g)g=h.nextSibling,f.appendChild(h)}return e} +for(;!a.nextSibling;)if(a=a.parentNode,!a)return;for(var a=b(a.nextSibling,0),e;(e=a.parentNode)&&e.nodeType===1;)a=e;d.push(a)}var k=/(?:^|\s)nocode(?:\s|$)/,t=/\r\n?|\n/,s=a.ownerDocument,l;a.currentStyle?l=a.currentStyle.whiteSpace:window.getComputedStyle&&(l=s.defaultView.getComputedStyle(a,q).getPropertyValue("white-space"));var p=l&&"pre"===l.substring(0,3);for(l=s.createElement("LI");a.firstChild;)l.appendChild(a.firstChild);for(var d=[l],g=0;g=0;){var h=m[e];A.hasOwnProperty(h)?window.console&&console.warn("cannot override language handler %s",h):A[h]=a}}function C(a,m){if(!a||!A.hasOwnProperty(a))a=/^\s*=o&&(h+=2);e>=c&&(a+=2)}}catch(w){"console"in window&&console.log(w&&w.stack?w.stack:w)}}var v=["break,continue,do,else,for,if,return,while"],w=[[v,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"], +"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],F=[w,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"],G=[w,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"], +H=[G,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"],w=[w,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"],I=[v,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"], +J=[v,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"],v=[v,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"],K=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/,N=/\S/,O=u({keywords:[F,H,w,"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END"+ +I,J,v],hashComments:!0,cStyleComments:!0,multiLineStrings:!0,regexLiterals:!0}),A={};k(O,["default-code"]);k(x([],[["pln",/^[^]*(?:>|$)/],["com",/^<\!--[\S\s]*?(?:--\>|$)/],["lang-",/^<\?([\S\s]+?)(?:\?>|$)/],["lang-",/^<%([\S\s]+?)(?:%>|$)/],["pun",/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\S\s]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\S\s]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\S\s]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]), +["default-markup","htm","html","mxml","xhtml","xml","xsl"]);k(x([["pln",/^\s+/,q," \t\r\n"],["atv",/^(?:"[^"]*"?|'[^']*'?)/,q,"\"'"]],[["tag",/^^<\/?[a-z](?:[\w-.:]*\w)?|\/?>$/i],["atn",/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^\s"'>]*(?:[^\s"'/>]|\/(?=\s)))/],["pun",/^[/<->]+/],["lang-js",/^on\w+\s*=\s*"([^"]+)"/i],["lang-js",/^on\w+\s*=\s*'([^']+)'/i],["lang-js",/^on\w+\s*=\s*([^\s"'>]+)/i],["lang-css",/^style\s*=\s*"([^"]+)"/i],["lang-css",/^style\s*=\s*'([^']+)'/i],["lang-css", +/^style\s*=\s*([^\s"'>]+)/i]]),["in.tag"]);k(x([],[["atv",/^[\S\s]+/]]),["uq.val"]);k(u({keywords:F,hashComments:!0,cStyleComments:!0,types:K}),["c","cc","cpp","cxx","cyc","m"]);k(u({keywords:"null,true,false"}),["json"]);k(u({keywords:H,hashComments:!0,cStyleComments:!0,verbatimStrings:!0,types:K}),["cs"]);k(u({keywords:G,cStyleComments:!0}),["java"]);k(u({keywords:v,hashComments:!0,multiLineStrings:!0}),["bsh","csh","sh"]);k(u({keywords:I,hashComments:!0,multiLineStrings:!0,tripleQuotedStrings:!0}), +["cv","py"]);k(u({keywords:"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["perl","pl","pm"]);k(u({keywords:J,hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["rb"]);k(u({keywords:w,cStyleComments:!0,regexLiterals:!0}),["js"]);k(u({keywords:"all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes", +hashComments:3,cStyleComments:!0,multilineStrings:!0,tripleQuotedStrings:!0,regexLiterals:!0}),["coffee"]);k(x([],[["str",/^[\S\s]+/]]),["regex"]);window.prettyPrintOne=function(a,m,e){var h=document.createElement("PRE");h.innerHTML=a;e&&D(h,e);E({g:m,i:e,h:h});return h.innerHTML};window.prettyPrint=function(a){function m(){for(var e=window.PR_SHOULD_USE_CONTINUATION?l.now()+250:Infinity;p=0){var k=k.match(g),f,b;if(b= +!k){b=n;for(var o=void 0,c=b.firstChild;c;c=c.nextSibling)var i=c.nodeType,o=i===1?o?b:c:i===3?N.test(c.nodeValue)?b:o:o;b=(f=o===b?void 0:o)&&"CODE"===f.tagName}b&&(k=f.className.match(g));k&&(k=k[1]);b=!1;for(o=n.parentNode;o;o=o.parentNode)if((o.tagName==="pre"||o.tagName==="code"||o.tagName==="xmp")&&o.className&&o.className.indexOf("prettyprint")>=0){b=!0;break}b||((b=(b=n.className.match(/\blinenums\b(?::(\d+))?/))?b[1]&&b[1].length?+b[1]:!0:!1)&&D(n,b),d={g:k,h:n,i:b},E(d))}}p + + + + + + + +
+ +

Introducing Guzzle

+ +

Guzzle takes the pain out of sending HTTP requests and the redundancy out of creating web service clients. It's + a framework that includes the tools needed to create a robust web service client, including: + Service descriptions for defining the inputs and outputs of an API, resource iterators for traversing + paginated resources, batching for sending a large number of requests as efficiently as possible.

+ +
    +
  • All the power of cURL with a simple interface.
  • +
  • Persistent connections and parallel requests.
  • +
  • Streams request and response bodies
  • +
  • Service descriptions for quickly building clients.
  • +
  • Powered by the Symfony2 EventDispatcher.
  • +
  • Use all of the code or only specific components.
  • +
  • Plugins for caching, logging, OAuth, mocks, and more +
  • Includes a custom node.js webserver to test your clients.
  • +
+ +
+ Guzzle is now part of Drupal 8 core and powers the official AWS SDK for PHP +
+ +

Quick Twitter example

+ +
<?php
+require_once 'vendor/autoload.php';
+use Guzzle\Http\Client;
+
+// Create a client and provide a base URL
+$client = new Client('https://api.github.com');
+// Create a request with basic Auth
+$request = $client->get('/user')->setAuth('user', 'pass');
+// Send the request and get the response
+$response = $request->send();
+echo $response->getBody();
+// >>> {"type":"User", ...
+echo $response->getHeader('Content-Length');
+// >>> 792
+
+// Create a client to work with the Twitter API
+$client = new Client('https://api.twitter.com/{version}', array(
+    'version' => '1.1'
+));
+
+// Sign all requests with the OauthPlugin
+$client->addSubscriber(new Guzzle\Plugin\Oauth\OauthPlugin(array(
+    'consumer_key'  => '***',
+    'consumer_secret' => '***',
+    'token'       => '***',
+    'token_secret'  => '***'
+)));
+
+echo $client->get('statuses/user_timeline.json')->send()->getBody();
+// >>> {"public_gists":6,"type":"User" ...
+
+// Create a tweet using POST
+$request = $client->post('statuses/update.json', null, array(
+    'status' => 'Tweeted with Guzzle, http://guzzlephp.org'
+));
+
+// Send the request and parse the JSON response into an array
+$data = $request->send()->json();
+echo $data['text'];
+// >>> Tweeted with Guzzle, http://t.co/kngJMfRk
+
+
+ + diff --git a/guzzle/guzzle/docs/_templates/leftbar.html b/guzzle/guzzle/docs/_templates/leftbar.html new file mode 100644 index 000000000..e69de29bb diff --git a/guzzle/guzzle/docs/_templates/nav_links.html b/guzzle/guzzle/docs/_templates/nav_links.html new file mode 100644 index 000000000..d4f2165bb --- /dev/null +++ b/guzzle/guzzle/docs/_templates/nav_links.html @@ -0,0 +1,5 @@ +
  • Docs
  • +
  • API
  • +
  • GitHub
  • +
  • Forum
  • +
  • IRC
  • diff --git a/guzzle/guzzle/docs/batching/batching.rst b/guzzle/guzzle/docs/batching/batching.rst new file mode 100644 index 000000000..57f04d80f --- /dev/null +++ b/guzzle/guzzle/docs/batching/batching.rst @@ -0,0 +1,183 @@ +======== +Batching +======== + +Guzzle provides a fairly generic and very customizable batching framework that allows developers to efficiently +transfer requests in parallel. + +Sending requests and commands in parallel +----------------------------------------- + +You can send HTTP requests in parallel by passing an array of ``Guzzle\Http\Message\RequestInterface`` objects to +``Guzzle\Http\Client::send()``: + +.. code-block:: php + + $responses = $client->send(array( + $client->get('http://www.example.com/foo'), + $client->get('http://www.example.com/baz') + $client->get('http://www.example.com/bar') + )); + +You can send commands in parallel by passing an array of ``Guzzle\Service\Command\CommandInterface`` objects +``Guzzle\Service\Client::execute()``: + +.. code-block:: php + + $commands = $client->execute(array( + $client->getCommand('foo'), + $client->getCommand('baz'), + $client->getCommand('bar') + )); + +These approaches work well for most use-cases. When you need more control over the requests that are sent in +parallel or you need to send a large number of requests, you need to use the functionality provided in the +``Guzzle\Batch`` namespace. + +Batching overview +----------------- + +The batch object, ``Guzzle\Batch\Batch``, is a queue. You add requests to the queue until you are ready to transfer +all of the requests. In order to efficiently transfer the items in the queue, the batch object delegates the +responsibility of dividing the queue into manageable parts to a divisor (``Guzzle\Batch\BatchDivisorInterface``). +The batch object then iterates over each array of items created by the divisor and sends them to the batch object's +``Guzzle\Batch\BatchTransferInterface``. + +.. code-block:: php + + use Guzzle\Batch\Batch; + use Guzzle\Http\BatchRequestTransfer; + + // BatchRequestTransfer acts as both the divisor and transfer strategy + $transferStrategy = new BatchRequestTransfer(10); + $divisorStrategy = $transferStrategy; + + $batch = new Batch($transferStrategy, $divisorStrategy); + + // Add some requests to the batch queue + $batch->add($request1) + ->add($request2) + ->add($request3); + + // Flush the queue and retrieve the flushed items + $arrayOfTransferredRequests = $batch->flush(); + +.. note:: + + You might find that your transfer strategy will need to act as both the divisor and transfer strategy. + +Using the BatchBuilder +---------------------- + +The ``Guzzle\Batch\BatchBuilder`` makes it easier to create batch objects. The batch builder also provides an easier +way to add additional behaviors to your batch object. + +Transferring requests +~~~~~~~~~~~~~~~~~~~~~ + +The ``Guzzle\Http\BatchRequestTransfer`` class efficiently transfers HTTP requests in parallel by grouping batches of +requests by the curl_multi handle that is used to transfer the requests. + +.. code-block:: php + + use Guzzle\Batch\BatchBuilder; + + $batch = BatchBuilder::factory() + ->transferRequests(10) + ->build(); + +Transferring commands +~~~~~~~~~~~~~~~~~~~~~ + +The ``Guzzle\Service\Command\BatchCommandTransfer`` class efficiently transfers service commands by grouping commands +by the client that is used to transfer them. You can add commands to a batch object that are transferred by different +clients, and the batch will handle the rest. + +.. code-block:: php + + use Guzzle\Batch\BatchBuilder; + + $batch = BatchBuilder::factory() + ->transferCommands(10) + ->build(); + + $batch->add($client->getCommand('foo')) + ->add($client->getCommand('baz')) + ->add($client->getCommand('bar')); + + $commands = $batch->flush(); + +Batch behaviors +--------------- + +You can add various behaviors to your batch that allow for more customizable transfers. + +Automatically flushing a queue +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Use the ``Guzzle\Batch\FlushingBatch`` decorator when you want to pump a large number of items into a batch queue and +have the queue automatically flush when the size of the queue reaches a certain threshold. + +.. code-block:: php + + use Guzzle\Batch\BatchBuilder; + + $batch = BatchBuilder::factory() + ->transferRequests(10) + ->autoFlushAt(10) + ->build(); + +Batch builder method: ``autoFlushAt($threshold)`` + +Notifying on flush +~~~~~~~~~~~~~~~~~~ + +Use the ``Guzzle\Batch\NotifyingBatch`` decorator if you want a function to be notified each time the batch queue is +flushed. This is useful when paired with the flushing batch decorator. Pass a callable to the ``notify()`` method of +a batch builder to use this decorator with the builder. + +.. code-block:: php + + use Guzzle\Batch\BatchBuilder; + + $batch = BatchBuilder::factory() + ->transferRequests(10) + ->autoFlushAt(10) + ->notify(function (array $transferredItems) { + echo 'Transferred ' . count($transferredItems) . "items\n"; + }) + ->build(); + +Batch builder method:: ``notify(callable $callback)`` + +Keeping a history +~~~~~~~~~~~~~~~~~ + +Use the ``Guzzle\Batch\HistoryBatch`` decorator if you want to maintain a history of all the items transferred with +the batch queue. + +.. code-block:: php + + use Guzzle\Batch\BatchBuilder; + + $batch = BatchBuilder::factory() + ->transferRequests(10) + ->keepHistory() + ->build(); + +After transferring items, you can use the ``getHistory()`` of a batch to retrieve an array of transferred items. Be +sure to periodically clear the history using ``clearHistory()``. + +Batch builder method: ``keepHistory()`` + +Exception buffering +~~~~~~~~~~~~~~~~~~~ + +Use the ``Guzzle\Batch\ExceptionBufferingBatch`` decorator to buffer exceptions during a transfer so that you can +transfer as many items as possible then deal with the errored batches after the transfer completes. After transfer, +use the ``getExceptions()`` method of a batch to retrieve an array of +``Guzzle\Batch\Exception\BatchTransferException`` objects. You can use these exceptions to attempt to retry the +failed batches. Be sure to clear the buffered exceptions when you are done with them by using the +``clearExceptions()`` method. + +Batch builder method: ``bufferExceptions()`` diff --git a/guzzle/guzzle/docs/conf.py b/guzzle/guzzle/docs/conf.py new file mode 100644 index 000000000..895886d24 --- /dev/null +++ b/guzzle/guzzle/docs/conf.py @@ -0,0 +1,95 @@ +import sys, os +from sphinx.highlighting import lexers +from pygments.lexers.web import PhpLexer + +lexers['php'] = PhpLexer(startinline=True, linenos=1) +lexers['php-annotations'] = PhpLexer(startinline=True, linenos=1) +primary_domain = 'php' + +# -- General configuration ----------------------------------------------------- + +extensions = [] +templates_path = ['_templates'] +source_suffix = '.rst' +master_doc = 'index' + +project = u'Guzzle' +copyright = u'2012, Michael Dowling' +version = '3.0.0' +release = '3.0.0' + +exclude_patterns = ['_build'] + +# -- Options for HTML output --------------------------------------------------- + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +html_title = "Guzzle documentation" +html_short_title = "Guzzle" + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Custom sidebar templates, maps document names to template names. +html_sidebars = { + '**': ['localtoc.html', 'leftbar.html', 'searchbox.html'] +} + +# Output file base name for HTML help builder. +htmlhelp_basename = 'Guzzledoc' + +# -- Guzzle Sphinx theme setup ------------------------------------------------ + +sys.path.insert(0, '/Users/dowling/projects/guzzle_sphinx_theme') + +import guzzle_sphinx_theme +pygments_style = 'guzzle_sphinx_theme.GuzzleStyle' +html_translator_class = 'guzzle_sphinx_theme.HTMLTranslator' +html_theme_path = guzzle_sphinx_theme.html_theme_path() +html_theme = 'guzzle_sphinx_theme' + +# Guzzle theme options (see theme.conf for more information) +html_theme_options = { + "index_template": "index.html", + "project_nav_name": "Guzzle", + "github_user": "guzzle", + "github_repo": "guzzle", + "disqus_comments_shortname": "guzzle", + "google_analytics_account": "UA-22752917-1" +} + +# -- Options for LaTeX output -------------------------------------------------- + +latex_elements = {} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass [howto/manual]). +latex_documents = [ + ('index', 'Guzzle.tex', u'Guzzle Documentation', + u'Michael Dowling', 'manual'), +] + +# -- Options for manual page output -------------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'guzzle', u'Guzzle Documentation', + [u'Michael Dowling'], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + +# -- Options for Texinfo output ------------------------------------------------ + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ('index', 'Guzzle', u'Guzzle Documentation', + u'Michael Dowling', 'Guzzle', 'One line description of project.', + 'Miscellaneous'), +] diff --git a/guzzle/guzzle/docs/docs.rst b/guzzle/guzzle/docs/docs.rst new file mode 100644 index 000000000..cf87908bd --- /dev/null +++ b/guzzle/guzzle/docs/docs.rst @@ -0,0 +1,73 @@ +.. title:: Guzzle | PHP HTTP client and framework for consuming RESTful web services + +==================== +Guzzle Documentation +==================== + +Getting started +--------------- + +.. toctree:: + :maxdepth: 1 + + getting-started/overview + getting-started/installation + getting-started/faq + +The HTTP client +--------------- + +.. toctree:: + :maxdepth: 2 + + http-client/client + http-client/request + http-client/response + http-client/entity-bodies + http-client/http-redirects + http-client/uri-templates + +Plugins +------- + +.. toctree:: + :maxdepth: 1 + + plugins/plugins-overview + plugins/creating-plugins + plugins/async-plugin + plugins/backoff-plugin + plugins/cache-plugin + plugins/cookie-plugin + plugins/curl-auth-plugin + plugins/history-plugin + plugins/log-plugin + plugins/md5-validator-plugin + plugins/mock-plugin + plugins/oauth-plugin + +The web service client +---------------------- + +.. toctree:: + :maxdepth: 1 + + webservice-client/webservice-client + webservice-client/using-the-service-builder + webservice-client/guzzle-service-descriptions + batching/batching + iterators/resource-iterators + iterators/guzzle-iterators + +Testing +------- + +.. toctree:: + :maxdepth: 2 + + testing/unit-testing + +API Docs +-------- + +`Read the API docs `_ diff --git a/guzzle/guzzle/docs/getting-started/faq.rst b/guzzle/guzzle/docs/getting-started/faq.rst new file mode 100644 index 000000000..a0a3fdbb6 --- /dev/null +++ b/guzzle/guzzle/docs/getting-started/faq.rst @@ -0,0 +1,29 @@ +=== +FAQ +=== + +What should I do if I get this error: Fatal error: Maximum function nesting level of '100' reached, aborting! +------------------------------------------------------------------------------------------------------------- + +You could run into this error if you have the XDebug extension installed and you execute a lot of requests in +callbacks. This error message comes specifically from the XDebug extension. PHP itself does not have a function +nesting limit. Change this setting in your php.ini to increase the limit:: + + xdebug.max_nesting_level = 1000 + +[`source `_] + +How can I speed up my client? +----------------------------- + +There are several things you can do to speed up your client: + +1. Utilize a C based HTTP message parser (e.g. ``Guzzle\Parser\Message\PeclHttpMessageParser``) +2. Disable operation validation by setting the ``command.disable_validation`` option to true on a command + +Why am I getting a 417 error response? +-------------------------------------- + +This can occur for a number of reasons, but if you are sending PUT, POST, or PATCH requests with an +``Expect: 100-Continue`` header, a server that does not support this header will return a 417 response. You can work +around this by calling ``$request->removeHeader('Expect');`` after setting the entity body of a request. diff --git a/guzzle/guzzle/docs/getting-started/installation.rst b/guzzle/guzzle/docs/getting-started/installation.rst new file mode 100644 index 000000000..7d522919b --- /dev/null +++ b/guzzle/guzzle/docs/getting-started/installation.rst @@ -0,0 +1,154 @@ +============ +Installation +============ + +Requirements +------------ + +#. PHP 5.3.3+ compiled with the cURL extension +#. A recent version of cURL 7.16.2+ compiled with OpenSSL and zlib + +Installing Guzzle +----------------- + +Composer +~~~~~~~~ + +The recommended way to install Guzzle is with `Composer `_. Composer is a dependency +management tool for PHP that allows you to declare the dependencies your project needs and installs them into your +project. + +.. code-block:: bash + + # Install Composer + curl -sS https://getcomposer.org/installer | php + + # Add Guzzle as a dependency + php composer.phar require guzzle/guzzle:~3.7 + +After installing, you need to require Composer's autoloader: + +.. code-block:: php + + require 'vendor/autoload.php'; + +You can find out more on how to install Composer, configure autoloading, and other best-practices for defining +dependencies at `getcomposer.org `_. + +Using only specific parts of Guzzle +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +While you can always just rely on ``guzzle/guzzle``, Guzzle provides several smaller parts of Guzzle as individual +packages available through Composer. + ++-----------------------------------------------------------------------------------------------+------------------------------------------+ +| Package name | Description | ++===============================================================================================+==========================================+ +| `guzzle/common `_ | Provides ``Guzzle\Common`` | ++-----------------------------------------------------------------------------------------------+------------------------------------------+ +| `guzzle/http `_ | Provides ``Guzzle\Http`` | ++-----------------------------------------------------------------------------------------------+------------------------------------------+ +| `guzzle/parser `_ | Provides ``Guzzle\Parser`` | ++-----------------------------------------------------------------------------------------------+------------------------------------------+ +| `guzzle/batch `_ | Provides ``Guzzle\Batch`` | ++-----------------------------------------------------------------------------------------------+------------------------------------------+ +| `guzzle/cache `_ | Provides ``Guzzle\Cache`` | ++-----------------------------------------------------------------------------------------------+------------------------------------------+ +| `guzzle/inflection `_ | Provides ``Guzzle\Inflection`` | ++-----------------------------------------------------------------------------------------------+------------------------------------------+ +| `guzzle/iterator `_ | Provides ``Guzzle\Iterator`` | ++-----------------------------------------------------------------------------------------------+------------------------------------------+ +| `guzzle/log `_ | Provides ``Guzzle\Log`` | ++-----------------------------------------------------------------------------------------------+------------------------------------------+ +| `guzzle/plugin `_ | Provides ``Guzzle\Plugin`` (all plugins) | ++-----------------------------------------------------------------------------------------------+------------------------------------------+ +| `guzzle/plugin-async `_ | Provides ``Guzzle\Plugin\Async`` | ++-----------------------------------------------------------------------------------------------+------------------------------------------+ +| `guzzle/plugin-backoff `_ | Provides ``Guzzle\Plugin\BackoffPlugin`` | ++-----------------------------------------------------------------------------------------------+------------------------------------------+ +| `guzzle/plugin-cache `_ | Provides ``Guzzle\Plugin\Cache`` | ++-----------------------------------------------------------------------------------------------+------------------------------------------+ +| `guzzle/plugin-cookie `_ | Provides ``Guzzle\Plugin\Cookie`` | ++-----------------------------------------------------------------------------------------------+------------------------------------------+ +| `guzzle/plugin-error-response `_ | Provides ``Guzzle\Plugin\ErrorResponse`` | ++-----------------------------------------------------------------------------------------------+------------------------------------------+ +| `guzzle/plugin-history `_ | Provides ``Guzzle\Plugin\History`` | ++-----------------------------------------------------------------------------------------------+------------------------------------------+ +| `guzzle/plugin-log `_ | Provides ``Guzzle\Plugin\Log`` | ++-----------------------------------------------------------------------------------------------+------------------------------------------+ +| `guzzle/plugin-md5 `_ | Provides ``Guzzle\Plugin\Md5`` | ++-----------------------------------------------------------------------------------------------+------------------------------------------+ +| `guzzle/plugin-mock `_ | Provides ``Guzzle\Plugin\Mock`` | ++-----------------------------------------------------------------------------------------------+------------------------------------------+ +| `guzzle/plugin-oauth `_ | Provides ``Guzzle\Plugin\Oauth`` | ++-----------------------------------------------------------------------------------------------+------------------------------------------+ +| `guzzle/service `_ | Provides ``Guzzle\Service`` | ++-----------------------------------------------------------------------------------------------+------------------------------------------+ +| `guzzle/stream `_ | Provides ``Guzzle\Stream`` | ++-----------------------------------------------------------------------------------------------+------------------------------------------+ + +Bleeding edge +^^^^^^^^^^^^^ + +During your development, you can keep up with the latest changes on the master branch by setting the version +requirement for Guzzle to ``dev-master``. + +.. code-block:: js + + { + "require": { + "guzzle/guzzle": "dev-master" + } + } + +PEAR +~~~~ + +Guzzle can be installed through PEAR: + +.. code-block:: bash + + pear channel-discover guzzlephp.org/pear + pear install guzzle/guzzle + +You can install a specific version of Guzzle by providing a version number suffix: + +.. code-block:: bash + + pear install guzzle/guzzle-3.7.0 + +Contributing to Guzzle +---------------------- + +In order to contribute, you'll need to checkout the source from GitHub and install Guzzle's dependencies using +Composer: + +.. code-block:: bash + + git clone https://github.com/guzzle/guzzle.git + cd guzzle && curl -s http://getcomposer.org/installer | php && ./composer.phar install --dev + +Guzzle is unit tested with PHPUnit. You will need to create your own phpunit.xml file in order to run the unit tests +(or just copy phpunit.xml.dist to phpunit.xml). Run the tests using the vendored PHPUnit binary: + +.. code-block:: bash + + vendor/bin/phpunit + +You'll need to install node.js v0.5.0 or newer in order to test the cURL implementation. + +Framework integrations +---------------------- + +Using Guzzle with Symfony +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Bundles are available on GitHub: + +- `DdeboerGuzzleBundle `_ for Guzzle 2 +- `MisdGuzzleBundle `_ for Guzzle 3 + +Using Guzzle with Silex +~~~~~~~~~~~~~~~~~~~~~~~ + +A `Guzzle Silex service provider `_ is available on GitHub. diff --git a/guzzle/guzzle/docs/getting-started/overview.rst b/guzzle/guzzle/docs/getting-started/overview.rst new file mode 100644 index 000000000..505b40978 --- /dev/null +++ b/guzzle/guzzle/docs/getting-started/overview.rst @@ -0,0 +1,85 @@ +================= +Welcome to Guzzle +================= + +What is Guzzle? +~~~~~~~~~~~~~~~ + +Guzzle is a PHP HTTP client and framework for building web service clients. Guzzle takes the pain out of sending HTTP +requests and the redundancy out of creating web service clients. + +Features at a glance +-------------------- + +- All the power of cURL with a simple interface. +- Persistent connections and parallel requests. +- Streams request and response bodies +- Service descriptions for quickly building clients. +- Powered by the Symfony2 EventDispatcher. +- Use all of the code or only specific components. +- Plugins for caching, logging, OAuth, mocks, and more +- Includes a custom node.js webserver to test your clients. +- Service descriptions for defining the inputs and outputs of an API +- Resource iterators for traversing paginated resources +- Batching for sending a large number of requests as efficiently as possible + +.. code-block:: php + + // Really simple using a static facade + Guzzle\Http\StaticClient::mount(); + $response = Guzzle::get('http://guzzlephp.org'); + + // More control using a client class + $client = new \Guzzle\Http\Client('http://guzzlephp.org'); + $request = $client->get('/'); + $response = $request->send(); + +License +------- + +Licensed using the `MIT license `_. + + Copyright (c) 2013 Michael Dowling + + 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. + +Contributing +------------ + +Guidelines +~~~~~~~~~~ + +This is still a work in progress, but there are only a few rules: + +1. Guzzle follows PSR-0, PSR-1, and PSR-2 +2. All pull requests must include unit tests to ensure the change works as expected and to prevent future regressions + +Reporting a security vulnerability +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +We want to ensure that Guzzle is a secure HTTP client library for everyone. If you've discovered a security +vulnerability in Guzzle, we appreciate your help in disclosing it to us in a +`responsible manner `_. + +Publicly disclosing a vulnerability can put the entire community at risk. If you've discovered a security concern, +please email us at security@guzzlephp.org. We'll work with you to make sure that we understand the scope of the issue, +and that we fully address your concern. We consider correspondence sent to security@guzzlephp.org our highest priority, +and work to address any issues that arise as quickly as possible. + +After a security vulnerability has been corrected, a security hotfix release will be deployed as soon as possible. diff --git a/guzzle/guzzle/docs/http-client/client.rst b/guzzle/guzzle/docs/http-client/client.rst new file mode 100644 index 000000000..d344a96e0 --- /dev/null +++ b/guzzle/guzzle/docs/http-client/client.rst @@ -0,0 +1,569 @@ +====================== +The Guzzle HTTP client +====================== + +Guzzle gives PHP developers complete control over HTTP requests while utilizing HTTP/1.1 best practices. Guzzle's HTTP +functionality is a robust framework built on top of the `PHP libcurl bindings `_. + +The three main parts of the Guzzle HTTP client are: + ++--------------+-------------------------------------------------------------------------------------------------------+ +| Clients | ``Guzzle\Http\Client`` (creates and sends requests, associates a response with a request) | ++--------------+-------------------------------------------------------------------------------------------------------+ +| Requests | ``Guzzle\Http\Message\Request`` (requests with no body), | +| | ``Guzzle\Http\Message\EntityEnclosingRequest`` (requests with a body) | ++--------------+-------------------------------------------------------------------------------------------------------+ +| Responses | ``Guzzle\Http\Message\Response`` | ++--------------+-------------------------------------------------------------------------------------------------------+ + +Creating a Client +----------------- + +Clients create requests, send requests, and set responses on a request object. When instantiating a client object, +you can pass an optional "base URL" and optional array of configuration options. A base URL is a +:doc:`URI template ` that contains the URL of a remote server. When creating requests with a relative +URL, the base URL of a client will be merged into the request's URL. + +.. code-block:: php + + use Guzzle\Http\Client; + + // Create a client and provide a base URL + $client = new Client('https://api.github.com'); + + $request = $client->get('/user'); + $request->setAuth('user', 'pass'); + echo $request->getUrl(); + // >>> https://api.github.com/user + + // You must send a request in order for the transfer to occur + $response = $request->send(); + + echo $response->getBody(); + // >>> {"type":"User", ... + + echo $response->getHeader('Content-Length'); + // >>> 792 + + $data = $response->json(); + echo $data['type']; + // >>> User + +Base URLs +~~~~~~~~~ + +Notice that the URL provided to the client's ``get()`` method is relative. Relative URLs will always merge into the +base URL of the client. There are a few rules that control how the URLs are merged. + +.. tip:: + + Guzzle follows `RFC 3986 `_ when merging base URLs and + relative URLs. + +In the above example, we passed ``/user`` to the ``get()`` method of the client. This is a relative URL, so it will +merge into the base URL of the client-- resulting in the derived URL of ``https://api.github.com/users``. + +``/user`` is a relative URL but uses an absolute path because it contains the leading slash. Absolute paths will +overwrite any existing path of the base URL. If an absolute path is provided (e.g. ``/path/to/something``), then the +path specified in the base URL of the client will be replaced with the absolute path, and the query string provided +by the relative URL will replace the query string of the base URL. + +Omitting the leading slash and using relative paths will add to the path of the base URL of the client. So using a +client base URL of ``https://api.twitter.com/v1.1`` and creating a GET request with ``statuses/user_timeline.json`` +will result in a URL of ``https://api.twitter.com/v1.1/statuses/user_timeline.json``. If a relative path and a query +string are provided, then the relative path will be appended to the base URL path, and the query string provided will +be merged into the query string of the base URL. + +If an absolute URL is provided (e.g. ``http://httpbin.org/ip``), then the request will completely use the absolute URL +as-is without merging in any of the URL parts specified in the base URL. + +Configuration options +~~~~~~~~~~~~~~~~~~~~~ + +The second argument of the client's constructor is an array of configuration data. This can include URI template data +or special options that alter the client's behavior: + ++-------------------------------+-------------------------------------------------------------------------------------+ +| ``request.options`` | Associative array of :ref:`Request options ` to apply to every | +| | request created by the client. | ++-------------------------------+-------------------------------------------------------------------------------------+ +| ``redirect.disable`` | Disable HTTP redirects for every request created by the client. | ++-------------------------------+-------------------------------------------------------------------------------------+ +| ``curl.options`` | Associative array of cURL options to apply to every request created by the client. | +| | if either the key or value of an entry in the array is a string, Guzzle will | +| | attempt to find a matching defined cURL constant automatically (e.g. | +| | "CURLOPT_PROXY" will be converted to the constant ``CURLOPT_PROXY``). | ++-------------------------------+-------------------------------------------------------------------------------------+ +| ``ssl.certificate_authority`` | Set to true to use the Guzzle bundled SSL certificate bundle (this is used by | +| | default, 'system' to use the bundle on your system, a string pointing to a file to | +| | use a specific certificate file, a string pointing to a directory to use multiple | +| | certificates, or ``false`` to disable SSL validation (not recommended). | +| | | +| | When using Guzzle inside of a phar file, the bundled SSL certificate will be | +| | extracted to your system's temp folder, and each time a client is created an MD5 | +| | check will be performed to ensure the integrity of the certificate. | ++-------------------------------+-------------------------------------------------------------------------------------+ +| ``command.params`` | When using a ``Guzzle\Service\Client`` object, this is an associative array of | +| | default options to set on each command created by the client. | ++-------------------------------+-------------------------------------------------------------------------------------+ + +Here's an example showing how to set various configuration options, including default headers to send with each request, +default query string parameters to add to each request, a default auth scheme for each request, and a proxy to use for +each request. Values can be injected into the client's base URL using variables from the configuration array. + +.. code-block:: php + + use Guzzle\Http\Client; + + $client = new Client('https://api.twitter.com/{version}', array( + 'version' => 'v1.1', + 'request.options' => array( + 'headers' => array('Foo' => 'Bar'), + 'query' => array('testing' => '123'), + 'auth' => array('username', 'password', 'Basic|Digest|NTLM|Any'), + 'proxy' => 'tcp://localhost:80' + ) + )); + +Setting a custom User-Agent +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The default Guzzle User-Agent header is ``Guzzle/ curl/ PHP/``. You can +customize the User-Agent header of a client by calling the ``setUserAgent()`` method of a Client object. + +.. code-block:: php + + // Completely override the default User-Agent + $client->setUserAgent('Test/123'); + + // Prepend a string to the default User-Agent + $client->setUserAgent('Test/123', true); + +Creating requests with a client +------------------------------- + +A Client object exposes several methods used to create Request objects: + +* Create a custom HTTP request: ``$client->createRequest($method, $uri, array $headers, $body, $options)`` +* Create a GET request: ``$client->get($uri, array $headers, $options)`` +* Create a HEAD request: ``$client->head($uri, array $headers, $options)`` +* Create a DELETE request: ``$client->delete($uri, array $headers, $body, $options)`` +* Create a POST request: ``$client->post($uri, array $headers, $postBody, $options)`` +* Create a PUT request: ``$client->put($uri, array $headers, $body, $options)`` +* Create a PATCH request: ``$client->patch($uri, array $headers, $body, $options)`` + +.. code-block:: php + + use Guzzle\Http\Client; + + $client = new Client('http://baseurl.com/api/v1'); + + // Create a GET request using Relative to base URL + // URL of the request: http://baseurl.com/api/v1/path?query=123&value=abc) + $request = $client->get('path?query=123&value=abc'); + $response = $request->send(); + + // Create HEAD request using a relative URL with an absolute path + // URL of the request: http://baseurl.com/path?query=123&value=abc + $request = $client->head('/path?query=123&value=abc'); + $response = $request->send(); + + // Create a DELETE request using an absolute URL + $request = $client->delete('http://www.example.com/path?query=123&value=abc'); + $response = $request->send(); + + // Create a PUT request using the contents of a PHP stream as the body + // Specify custom HTTP headers + $request = $client->put('http://www.example.com/upload', array( + 'X-Header' => 'My Header' + ), fopen('http://www.test.com/', 'r')); + $response = $request->send(); + + // Create a POST request and add the POST files manually + $request = $client->post('http://localhost:8983/solr/update') + ->addPostFiles(array('file' => '/path/to/documents.xml')); + $response = $request->send(); + + // Check if a resource supports the DELETE method + $supportsDelete = $client->options('/path')->send()->isMethodAllowed('DELETE'); + $response = $request->send(); + +Client objects create Request objects using a request factory (``Guzzle\Http\Message\RequestFactoryInterface``). +You can inject a custom request factory into the Client using ``$client->setRequestFactory()``, but you can typically +rely on a Client's default request factory. + +Static clients +-------------- + +You can use Guzzle's static client facade to more easily send simple HTTP requests. + +.. code-block:: php + + // Mount the client so that you can access it at \Guzzle + Guzzle\Http\StaticClient::mount(); + $response = Guzzle::get('http://guzzlephp.org'); + +Each request method of the static client (e.g. ``get()``, ``post()`, ``put()``, etc) accepts an associative array of request +options to apply to the request. + +.. code-block:: php + + $response = Guzzle::post('http://test.com', array( + 'headers' => array('X-Foo' => 'Bar'), + 'body' => array('Test' => '123'), + 'timeout' => 10 + )); + +.. _request-options: + +Request options +--------------- + +Request options can be specified when creating a request or in the ``request.options`` parameter of a client. These +options can control various aspects of a request including: headers to send, query string data, where the response +should be downloaded, proxies, auth, etc. + +headers +~~~~~~~ + +Associative array of headers to apply to the request. When specified in the ``$options`` argument of a client creational +method (e.g. ``get()``, ``post()``, etc), the headers in the ``$options`` array will overwrite headers specified in the +``$headers`` array. + +.. code-block:: php + + $request = $client->get($url, array(), array( + 'headers' => array('X-Foo' => 'Bar') + )); + +Headers can be specified on a client to add default headers to every request sent by a client. + +.. code-block:: php + + $client = new Guzzle\Http\Client(); + + // Set a single header using path syntax + $client->setDefaultOption('headers/X-Foo', 'Bar'); + + // Set all headers + $client->setDefaultOption('headers', array('X-Foo' => 'Bar')); + +.. note:: + + In addition to setting request options when creating requests or using the ``setDefaultOption()`` method, any + default client request option can be set using a client's config object: + + .. code-block:: php + + $client->getConfig()->setPath('request.options/headers/X-Foo', 'Bar'); + +query +~~~~~ + +Associative array of query string parameters to the request. When specified in the ``$options`` argument of a client +creational method, the query string parameters in the ``$options`` array will overwrite query string parameters +specified in the `$url`. + +.. code-block:: php + + $request = $client->get($url, array(), array( + 'query' => array('abc' => '123') + )); + +Query string parameters can be specified on a client to add default query string parameters to every request sent by a +client. + +.. code-block:: php + + $client = new Guzzle\Http\Client(); + + // Set a single query string parameter using path syntax + $client->setDefaultOption('query/abc', '123'); + + // Set an array of default query string parameters + $client->setDefaultOption('query', array('abc' => '123')); + +body +~~~~ + +Sets the body of a request. The value supplied to the body option can be a ``Guzzle\Http\EntityBodyInterface``, string, +fopen resource, or array when sending POST requests. When a ``body`` request option is supplied, the option value will +overwrite the ``$body`` argument of a client creational method. + +auth +~~~~ + +Specifies and array of HTTP authorization parameters parameters to use with the request. The array must contain the +username in index [0], the password in index [1], and can optionally contain the authentication type in index [2]. +The available authentication types are: "Basic" (default), "Digest", "NTLM", or "Any". + +.. code-block:: php + + $request = $client->get($url, array(), array( + 'auth' => array('username', 'password', 'Digest') + )); + + // You can add auth headers to every request of a client + $client->setDefaultOption('auth', array('username', 'password', 'Digest')); + +cookies +~~~~~~~ + +Specifies an associative array of cookies to add to the request. + +allow_redirects +~~~~~~~~~~~~~~~ + +Specifies whether or not the request should follow redirects. Requests will follow redirects by default. Set +``allow_redirects`` to ``false`` to disable redirects. + +save_to +~~~~~~~ + +The ``save_to`` option specifies where the body of a response is downloaded. You can pass the path to a file, an fopen +resource, or a ``Guzzle\Http\EntityBodyInterface`` object. + +See :ref:`Changing where a response is downloaded ` for more information on setting the +`save_to` option. + +events +~~~~~~ + +The `events` option makes it easy to attach listeners to the various events emitted by a request object. The `events` +options must be an associative array mapping an event name to a Closure or array the contains a Closure and the +priority of the event. + +.. code-block:: php + + $request = $client->get($url, array(), array( + 'events' => array( + 'request.before_send' => function (\Guzzle\Common\Event $e) { + echo 'About to send ' . $e['request']; + } + ) + )); + + // Using the static client: + Guzzle::get($url, array( + 'events' => array( + 'request.before_send' => function (\Guzzle\Common\Event $e) { + echo 'About to send ' . $e['request']; + } + ) + )); + +plugins +~~~~~~~ + +The `plugins` options makes it easy to attach an array of plugins to a request. + +.. code-block:: php + + // Using the static client: + Guzzle::get($url, array( + 'plugins' => array( + new Guzzle\Plugin\Cache\CachePlugin(), + new Guzzle\Plugin\Cookie\CookiePlugin() + ) + )); + +exceptions +~~~~~~~~~~ + +The `exceptions` option can be used to disable throwing exceptions for unsuccessful HTTP response codes +(e.g. 404, 500, etc). Set `exceptions` to false to not throw exceptions. + +params +~~~~~~ + +The `params` options can be used to specify an associative array of data parameters to add to a request. Note that +these are not query string parameters. + +timeout / connect_timeout +~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can specify the maximum number of seconds to allow for an entire transfer to take place before timing out using +the `timeout` request option. You can specify the maximum number of seconds to wait while trying to connect using the +`connect_timeout` request option. Set either of these options to 0 to wait indefinitely. + +.. code-block:: php + + $request = $client->get('http://www.example.com', array(), array( + 'timeout' => 20, + 'connect_timeout' => 1.5 + )); + +verify +~~~~~~ + +Set to true to enable SSL certificate validation (the default), false to disable SSL certificate validation, or supply +the path to a CA bundle to enable verification using a custom certificate. + +cert +~~~~ + +The `cert` option lets you specify a PEM formatted SSL client certificate to use with servers that require one. If the +certificate requires a password, provide an array with the password as the second item. + +This would typically be used in conjuction with the `ssl_key` option. + +.. code-block:: php + + $request = $client->get('https://www.example.com', array(), array( + 'cert' => '/etc/pki/client_certificate.pem' + ) + + $request = $client->get('https://www.example.com', array(), array( + 'cert' => array('/etc/pki/client_certificate.pem', 's3cr3tp455w0rd') + ) + +ssl_key +~~~~~~~ + +The `ssl_key` option lets you specify a file containing your PEM formatted private key, optionally protected by a password. +Note: your password is sensitive, keep the PHP script containing it safe. + +This would typically be used in conjuction with the `cert` option. + +.. code-block:: php + + $request = $client->get('https://www.example.com', array(), array( + 'ssl_key' => '/etc/pki/private_key.pem' + ) + + $request = $client->get('https://www.example.com', array(), array( + 'ssl_key' => array('/etc/pki/private_key.pem', 's3cr3tp455w0rd') + ) + +proxy +~~~~~ + +The `proxy` option is used to specify an HTTP proxy (e.g. `http://username:password@192.168.16.1:10`). + +debug +~~~~~ + +The `debug` option is used to show verbose cURL output for a transfer. + +stream +~~~~~~ + +When using a static client, you can set the `stream` option to true to return a `Guzzle\Stream\Stream` object that can +be used to pull data from a stream as needed (rather than have cURL download the entire contents of a response to a +stream all at once). + +.. code-block:: php + + $stream = Guzzle::get('http://guzzlephp.org', array('stream' => true)); + while (!$stream->feof()) { + echo $stream->readLine(); + } + +Sending requests +---------------- + +Requests can be sent by calling the ``send()`` method of a Request object, but you can also send requests using the +``send()`` method of a Client. + +.. code-block:: php + + $request = $client->get('http://www.amazon.com'); + $response = $client->send($request); + +Sending requests in parallel +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The Client's ``send()`` method accept a single ``Guzzle\Http\Message\RequestInterface`` object or an array of +RequestInterface objects. When an array is specified, the requests will be sent in parallel. + +Sending many HTTP requests serially (one at a time) can cause an unnecessary delay in a script's execution. Each +request must complete before a subsequent request can be sent. By sending requests in parallel, a pool of HTTP +requests can complete at the speed of the slowest request in the pool, significantly reducing the amount of time +needed to execute multiple HTTP requests. Guzzle provides a wrapper for the curl_multi functions in PHP. + +Here's an example of sending three requests in parallel using a client object: + +.. code-block:: php + + use Guzzle\Common\Exception\MultiTransferException; + + try { + $responses = $client->send(array( + $client->get('http://www.google.com/'), + $client->head('http://www.google.com/'), + $client->get('https://www.github.com/') + )); + } catch (MultiTransferException $e) { + + echo "The following exceptions were encountered:\n"; + foreach ($e as $exception) { + echo $exception->getMessage() . "\n"; + } + + echo "The following requests failed:\n"; + foreach ($e->getFailedRequests() as $request) { + echo $request . "\n\n"; + } + + echo "The following requests succeeded:\n"; + foreach ($e->getSuccessfulRequests() as $request) { + echo $request . "\n\n"; + } + } + +If the requests succeed, an array of ``Guzzle\Http\Message\Response`` objects are returned. A single request failure +will not cause the entire pool of requests to fail. Any exceptions thrown while transferring a pool of requests will +be aggregated into a ``Guzzle\Common\Exception\MultiTransferException`` exception. + +Plugins and events +------------------ + +Guzzle provides easy to use request plugins that add behavior to requests based on signal slot event notifications +powered by the +`Symfony2 Event Dispatcher component `_. Any +event listener or subscriber attached to a Client object will automatically be attached to each request created by the +client. + +Using the same cookie session for each request +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Attach a ``Guzzle\Plugin\Cookie\CookiePlugin`` to a client which will in turn add support for cookies to every request +created by a client, and each request will use the same cookie session: + +.. code-block:: php + + use Guzzle\Plugin\Cookie\CookiePlugin; + use Guzzle\Plugin\Cookie\CookieJar\ArrayCookieJar; + + // Create a new cookie plugin + $cookiePlugin = new CookiePlugin(new ArrayCookieJar()); + + // Add the cookie plugin to the client + $client->addSubscriber($cookiePlugin); + +.. _client-events: + +Events emitted from a client +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A ``Guzzle\Http\Client`` object emits the following events: + ++------------------------------+--------------------------------------------+------------------------------------------+ +| Event name | Description | Event data | ++==============================+============================================+==========================================+ +| client.create_request | Called when a client creates a request | * client: The client | +| | | * request: The created request | ++------------------------------+--------------------------------------------+------------------------------------------+ + +.. code-block:: php + + use Guzzle\Common\Event; + use Guzzle\Http\Client; + + $client = new Client(); + + // Add a listener that will echo out requests as they are created + $client->getEventDispatcher()->addListener('client.create_request', function (Event $e) { + echo 'Client object: ' . spl_object_hash($e['client']) . "\n"; + echo "Request object: {$e['request']}\n"; + }); diff --git a/guzzle/guzzle/docs/http-client/entity-bodies.rst b/guzzle/guzzle/docs/http-client/entity-bodies.rst new file mode 100644 index 000000000..823b0c022 --- /dev/null +++ b/guzzle/guzzle/docs/http-client/entity-bodies.rst @@ -0,0 +1,151 @@ +=========================== +Request and response bodies +=========================== + +`Entity body `_ is the term used for the body of an HTTP +message. The entity body of requests and responses is inherently a +`PHP stream `_ in Guzzle. The body of the request can be either a string or +a PHP stream which are converted into a ``Guzzle\Http\EntityBody`` object using its factory method. When using a +string, the entity body is stored in a `temp PHP stream `_. The use of +temp PHP streams helps to protect your application from running out of memory when sending or receiving large entity +bodies in your messages. When more than 2MB of data is stored in a temp stream, it automatically stores the data on +disk rather than in memory. + +EntityBody objects provide a great deal of functionality: compression, decompression, calculate the Content-MD5, +calculate the Content-Length (when the resource is repeatable), guessing the Content-Type, and more. Guzzle doesn't +need to load an entire entity body into a string when sending or retrieving data; entity bodies are streamed when +being uploaded and downloaded. + +Here's an example of gzip compressing a text file then sending the file to a URL: + +.. code-block:: php + + use Guzzle\Http\EntityBody; + + $body = EntityBody::factory(fopen('/path/to/file.txt', 'r+')); + echo $body->read(1024); + $body->seek(0, SEEK_END); + $body->write('foo'); + echo $body->ftell(); + $body->rewind(); + + // Send a request using the body + $response = $client->put('http://localhost:8080/uploads', null, $body)->send(); + +The body of the request can be specified in the ``Client::put()`` or ``Client::post()`` method, or, you can specify +the body of the request by calling the ``setBody()`` method of any +``Guzzle\Http\Message\EntityEnclosingRequestInterface`` object. + +Compression +----------- + +You can compress the contents of an EntityBody object using the ``compress()`` method. The compress method accepts a +filter that must match to one of the supported +`PHP stream filters `_ on your system (e.g. `zlib.deflate`, +``bzip2.compress``, etc). Compressing an entity body will stream the entire entity body through a stream compression +filter into a temporary PHP stream. You can uncompress an entity body using the ``uncompress()`` method and passing +the PHP stream filter to use when decompressing the stream (e.g. ``zlib.inflate``). + +.. code-block:: php + + use Guzzle\Http\EntityBody; + + $body = EntityBody::factory(fopen('/tmp/test.txt', 'r+')); + echo $body->getSize(); + // >>> 1048576 + + // Compress using the default zlib.deflate filter + $body->compress(); + echo $body->getSize(); + // >>> 314572 + + // Decompress the stream + $body->uncompress(); + echo $body->getSize(); + // >>> 1048576 + +Decorators +---------- + +Guzzle provides several EntityBody decorators that can be used to add functionality to an EntityBody at runtime. + +IoEmittingEntityBody +~~~~~~~~~~~~~~~~~~~~ + +This decorator will emit events when data is read from a stream or written to a stream. Add an event subscriber to the +entity body's ``body.read`` or ``body.write`` methods to receive notifications when data data is transferred. + +.. code-block:: php + + use Guzzle\Common\Event; + use Guzzle\Http\EntityBody; + use Guzzle\Http\IoEmittingEntityBody; + + $original = EntityBody::factory(fopen('/tmp/test.txt', 'r+')); + $body = new IoEmittingEntityBody($original); + + // Listen for read events + $body->getEventDispatcher()->addListener('body.read', function (Event $e) { + // Grab data from the event + $entityBody = $e['body']; + // Amount of data retrieved from the body + $lengthOfData = $e['length']; + // The actual data that was read + $data = $e['read']; + }); + + // Listen for write events + $body->getEventDispatcher()->addListener('body.write', function (Event $e) { + // Grab data from the event + $entityBody = $e['body']; + // The data that was written + $data = $e['write']; + // The actual amount of data that was written + $data = $e['read']; + }); + +ReadLimitEntityBody +~~~~~~~~~~~~~~~~~~~ + +The ReadLimitEntityBody decorator can be used to transfer a subset or slice of an existing EntityBody object. This can +be useful for breaking a large file into smaller pieces to be sent in chunks (e.g. Amazon S3's multipart upload API). + +.. code-block:: php + + use Guzzle\Http\EntityBody; + use Guzzle\Http\ReadLimitEntityBody; + + $original = EntityBody::factory(fopen('/tmp/test.txt', 'r+')); + echo $original->getSize(); + // >>> 1048576 + + // Limit the size of the body to 1024 bytes and start reading from byte 2048 + $body = new ReadLimitEntityBody($original, 1024, 2048); + echo $body->getSize(); + // >>> 1024 + echo $body->ftell(); + // >>> 0 + +CachingEntityBody +~~~~~~~~~~~~~~~~~ + +The CachingEntityBody decorator is used to allow seeking over previously read bytes on non-seekable read streams. This +can be useful when transferring a non-seekable entity body fails due to needing to rewind the stream (for example, +resulting from a redirect). Data that is read from the remote stream will be buffered in a PHP temp stream so that +previously read bytes are cached first in memory, then on disk. + +.. code-block:: php + + use Guzzle\Http\EntityBody; + use Guzzle\Http\CachingEntityBody; + + $original = EntityBody::factory(fopen('http://www.google.com', 'r')); + $body = new CachingEntityBody($original); + + $body->read(1024); + echo $body->ftell(); + // >>> 1024 + + $body->seek(0); + echo $body->ftell(); + // >>> 0 diff --git a/guzzle/guzzle/docs/http-client/http-redirects.rst b/guzzle/guzzle/docs/http-client/http-redirects.rst new file mode 100644 index 000000000..32ba26891 --- /dev/null +++ b/guzzle/guzzle/docs/http-client/http-redirects.rst @@ -0,0 +1,99 @@ +============== +HTTP redirects +============== + +By default, Guzzle will automatically follow redirects using the non-RFC compliant implementation used by most web +browsers. This means that redirects for POST requests are followed by a GET request. You can force RFC compliance by +enabling the strict mode on a request's parameter object: + +.. code-block:: php + + // Set per request + $request = $client->post(); + $request->getParams()->set('redirect.strict', true); + + // You can set globally on a client so all requests use strict redirects + $client->getConfig()->set('request.params', array( + 'redirect.strict' => true + )); + +By default, Guzzle will redirect up to 5 times before throwing a ``Guzzle\Http\Exception\TooManyRedirectsException``. +You can raise or lower this value using the ``redirect.max`` parameter of a request object: + +.. code-block:: php + + $request->getParams()->set('redirect.max', 2); + +Redirect history +---------------- + +You can get the number of redirects of a request using the resulting response object's ``getRedirectCount()`` method. +Similar to cURL's ``effective_url`` property, Guzzle provides the effective URL, or the last redirect URL that returned +the request, in a response's ``getEffectiveUrl()`` method. + +When testing or debugging, it is often useful to see a history of redirects for a particular request. This can be +achieved using the HistoryPlugin. + +.. code-block:: php + + $request = $client->get('/'); + $history = new Guzzle\Plugin\History\HistoryPlugin(); + $request->addSubscriber($history); + $response = $request->send(); + + // Get the last redirect URL or the URL of the request that received + // this response + echo $response->getEffectiveUrl(); + + // Get the number of redirects + echo $response->getRedirectCount(); + + // Iterate over each sent request and response + foreach ($history->getAll() as $transaction) { + // Request object + echo $transaction['request']->getUrl() . "\n"; + // Response object + echo $transaction['response']->getEffectiveUrl() . "\n"; + } + + // Or, simply cast the HistoryPlugin to a string to view each request and response + echo $history; + +Disabling redirects +------------------- + +You can disable redirects on a client by passing a configuration option in the client's constructor: + +.. code-block:: php + + $client = new Client(null, array('redirect.disable' => true)); + +You can also disable redirects per request: + +.. code-block:: php + + $request = $client->get($url, array(), array('allow_redirects' => false)); + +Redirects and non-repeatable streams +------------------------------------ + +If you are redirected when sending data from a non-repeatable stream and some of the data has been read off of the +stream, then you will get a ``Guzzle\Http\Exception\CouldNotRewindStreamException``. You can get around this error by +adding a custom rewind method to the entity body object being sent in the request. + +.. code-block:: php + + $request = $client->post( + 'http://httpbin.com/redirect/2', + null, + fopen('http://httpbin.com/get', 'r') + ); + + // Add a custom function that can be used to rewind the stream + // (reopen in this example) + $request->getBody()->setRewindFunction(function ($body) { + $body->setStream(fopen('http://httpbin.com/get', 'r')); + return true; + ); + + $response = $client->send(); diff --git a/guzzle/guzzle/docs/http-client/request.rst b/guzzle/guzzle/docs/http-client/request.rst new file mode 100644 index 000000000..a0593c915 --- /dev/null +++ b/guzzle/guzzle/docs/http-client/request.rst @@ -0,0 +1,667 @@ +===================== +Using Request objects +===================== + +HTTP request messages +--------------------- + +Request objects are all about building an HTTP message. Each part of an HTTP request message can be set individually +using methods on the request object or set in bulk using the ``setUrl()`` method. Here's the format of an HTTP request +with each part of the request referencing the method used to change it:: + + PUT(a) /path(b)?query=123(c) HTTP/1.1(d) + X-Header(e): header + Content-Length(e): 4 + + data(f) + ++-------------------------+---------------------------------------------------------------------------------+ +| a. **Method** | The request method can only be set when instantiating a request | ++-------------------------+---------------------------------------------------------------------------------+ +| b. **Path** | ``$request->setPath('/path');`` | ++-------------------------+---------------------------------------------------------------------------------+ +| c. **Query** | ``$request->getQuery()->set('query', '123');`` | ++-------------------------+---------------------------------------------------------------------------------+ +| d. **Protocol version** | ``$request->setProtocolVersion('1.1');`` | ++-------------------------+---------------------------------------------------------------------------------+ +| e. **Header** | ``$request->setHeader('X-Header', 'header');`` | ++-------------------------+---------------------------------------------------------------------------------+ +| f. **Entity Body** | ``$request->setBody('data'); // Only available with PUT, POST, PATCH, DELETE`` | ++-------------------------+---------------------------------------------------------------------------------+ + +Creating requests with a client +------------------------------- + +Client objects are responsible for creating HTTP request objects. + +GET requests +~~~~~~~~~~~~ + +`GET requests `_ are the most common form of HTTP +requests. When you visit a website in your browser, the HTML of the website is downloaded using a GET request. GET +requests are idempotent requests that are typically used to download content (an entity) identified by a request URL. + +.. code-block:: php + + use Guzzle\Http\Client; + + $client = new Client(); + + // Create a request that has a query string and an X-Foo header + $request = $client->get('http://www.amazon.com?a=1', array('X-Foo' => 'Bar')); + + // Send the request and get the response + $response = $request->send(); + +You can change where the body of a response is downloaded on any request using the +``$request->setResponseBody(string|EntityBodyInterface|resource)`` method of a request. You can also set the ``save_to`` +option of a request: + +.. code-block:: php + + // Send the response body to a file + $request = $client->get('http://test.com', array(), array('save_to' => '/path/to/file')); + + // Send the response body to an fopen resource + $request = $client->get('http://test.com', array(), array('save_to' => fopen('/path/to/file', 'w'))); + +HEAD requests +~~~~~~~~~~~~~ + +`HEAD requests `_ work exactly like GET requests except +that they do not actually download the response body (entity) of the response message. HEAD requests are useful for +retrieving meta information about an entity identified by a Request-URI. + +.. code-block:: php + + $client = new Guzzle\Http\Client(); + $request = $client->head('http://www.amazon.com'); + $response = $request->send(); + echo $response->getContentLength(); + // >>> Will output the Content-Length header value + +DELETE requests +~~~~~~~~~~~~~~~ + +A `DELETE method `_ requests that the origin server +delete the resource identified by the Request-URI. + +.. code-block:: php + + $client = new Guzzle\Http\Client(); + $request = $client->delete('http://example.com'); + $response = $request->send(); + +POST requests +~~~~~~~~~~~~~ + +While `POST requests `_ can be used for a number of +reasons, POST requests are often used when submitting HTML form data to a website. POST requests can include an entity +body in the HTTP request. + +POST requests in Guzzle are sent with an ``application/x-www-form-urlencoded`` Content-Type header if POST fields are +present but no files are being sent in the POST. If files are specified in the POST request, then the Content-Type +header will become ``multipart/form-data``. + +The ``post()`` method of a client object accepts four arguments: the URL, optional headers, post fields, and an array of +request options. To send files in the POST request, prepend the ``@`` symbol to the array value (just like you would if +you were using the PHP ``curl_setopt`` function). + +Here's how to create a multipart/form-data POST request containing files and fields: + +.. code-block:: php + + $request = $client->post('http://httpbin.org/post', array(), array( + 'custom_field' => 'my custom value', + 'file_field' => '@/path/to/file.xml' + )); + + $response = $request->send(); + +.. note:: + + Remember to **always** sanitize user input when sending POST requests: + + .. code-block:: php + + // Prevent users from accessing sensitive files by sanitizing input + $_POST = array('firstname' => '@/etc/passwd'); + $request = $client->post('http://www.example.com', array(), array ( + 'firstname' => str_replace('@', '', $_POST['firstname']) + )); + +You can alternatively build up the contents of a POST request. + +.. code-block:: php + + $request = $client->post('http://httpbin.org/post') + ->setPostField('custom_field', 'my custom value') + ->addPostFile('file', '/path/to/file.xml'); + + $response = $request->send(); + +Raw POST data +^^^^^^^^^^^^^ + +POST requests can also contain raw POST data that is not related to HTML forms. + +.. code-block:: php + + $request = $client->post('http://httpbin.org/post', array(), 'this is the body'); + $response = $request->send(); + +You can set the body of POST request using the ``setBody()`` method of the +``Guzzle\Http\Message\EntityEnclosingRequest`` object. This method accepts a string, a resource returned from +``fopen``, or a ``Guzzle\Http\EntityBodyInterface`` object. + +.. code-block:: php + + $request = $client->post('http://httpbin.org/post'); + // Set the body of the POST to stream the contents of /path/to/large_body.txt + $request->setBody(fopen('/path/to/large_body.txt', 'r')); + $response = $request->send(); + +PUT requests +~~~~~~~~~~~~ + +The `PUT method `_ requests that the enclosed entity be +stored under the supplied Request-URI. PUT requests are similar to POST requests in that they both can send an entity +body in the request message. + +The body of a PUT request (any any ``Guzzle\Http\Message\EntityEnclosingRequestInterface`` object) is always stored as +a ``Guzzle\Http\Message\EntityBodyInterface`` object. This allows a great deal of flexibility when sending data to a +remote server. For example, you can stream the contents of a stream returned by fopen, stream the contents of a +callback function, or simply send a string of data. + +.. code-block:: php + + $request = $client->put('http://httpbin.org/put', array(), 'this is the body'); + $response = $request->send(); + +Just like with POST, PATH, and DELETE requests, you can set the body of a PUT request using the ``setBody()`` method. + +.. code-block:: php + + $request = $client->put('http://httpbin.org/put'); + $request->setBody(fopen('/path/to/large_body.txt', 'r')); + $response = $request->send(); + +PATCH requests +~~~~~~~~~~~~~~ + +`PATCH requests `_ are used to modify a resource. + +.. code-block:: php + + $request = $client->patch('http://httpbin.org', array(), 'this is the body'); + $response = $request->send(); + +OPTIONS requests +~~~~~~~~~~~~~~~~ + +The `OPTIONS method `_ represents a request for +information about the communication options available on the request/response chain identified by the Request-URI. + +.. code-block:: php + + $request = $client->options('http://httpbin.org'); + $response = $request->send(); + + // Check if the PUT method is supported by this resource + var_export($response->isMethodAllows('PUT')); + +Custom requests +~~~~~~~~~~~~~~~ + +You can create custom HTTP requests that use non-standard HTTP methods using the ``createRequest()`` method of a +client object. + +.. code-block:: php + + $request = $client->createRequest('COPY', 'http://example.com/foo', array( + 'Destination' => 'http://example.com/bar', + 'Overwrite' => 'T' + )); + $response = $request->send(); + +Query string parameters +----------------------- + +Query string parameters of a request are owned by a request's ``Guzzle\Http\Query`` object that is accessible by +calling ``$request->getQuery()``. The Query class extends from ``Guzzle\Common\Collection`` and allows you to set one +or more query string parameters as key value pairs. You can set a parameter on a Query object using the +``set($key, $value)`` method or access the query string object like an associative array. Any previously specified +value for a key will be overwritten when using ``set()``. Use ``add($key, $value)`` to add a value to query string +object, and in the event of a collision with an existing value at a specific key, the value will be converted to an +array that contains all of the previously set values. + +.. code-block:: php + + $request = new Guzzle\Http\Message\Request('GET', 'http://www.example.com?foo=bar&abc=123'); + + $query = $request->getQuery(); + echo "{$query}\n"; + // >>> foo=bar&abc=123 + + $query->remove('abc'); + echo "{$query}\n"; + // >>> foo=bar + + $query->set('foo', 'baz'); + echo "{$query}\n"; + // >>> foo=baz + + $query->add('foo', 'bar'); + echo "{$query}\n"; + // >>> foo%5B0%5D=baz&foo%5B1%5D=bar + +Whoah! What happened there? When ``foo=bar`` was added to the existing ``foo=baz`` query string parameter, the +aggregator associated with the Query object was used to help convert multi-value query string parameters into a string. +Let's disable URL-encoding to better see what's happening. + +.. code-block:: php + + $query->useUrlEncoding(false); + echo "{$query}\n"; + // >>> foo[0]=baz&foo[1]=bar + +.. note:: + + URL encoding can be disabled by passing false, enabled by passing true, set to use RFC 1738 by passing + ``Query::FORM_URLENCODED`` (internally uses PHP's ``urlencode`` function), or set to RFC 3986 by passing + ``Query::RFC_3986`` (this is the default and internally uses PHP's ``rawurlencode`` function). + +As you can see, the multiple values were converted into query string parameters following the default PHP convention of +adding numerically indexed square bracket suffixes to each key (``foo[0]=baz&foo[1]=bar``). The strategy used to convert +multi-value parameters into a string can be customized using the ``setAggregator()`` method of the Query class. Guzzle +ships with the following query string aggregators by default: + +1. ``Guzzle\Http\QueryAggregator\PhpAggregator``: Aggregates using PHP style brackets (e.g. ``foo[0]=baz&foo[1]=bar``) +2. ``Guzzle\Http\QueryAggregator\DuplicateAggregator``: Performs no aggregation and allows for key value pairs to be + repeated in a URL (e.g. ``foo=baz&foo=bar``) +3. ``Guzzle\Http\QueryAggregator\CommaAggregator``: Aggregates using commas (e.g. ``foo=baz,bar``) + +.. _http-message-headers: + +HTTP Message Headers +-------------------- + +HTTP message headers are case insensitive, multiple occurrences of any header can be present in an HTTP message +(whether it's valid or not), and some servers require specific casing of particular headers. Because of this, request +and response headers are stored in ``Guzzle\Http\Message\Header`` objects. The Header object can be cast as a string, +counted, or iterated to retrieve each value from the header. Casting a Header object to a string will return all of +the header values concatenated together using a glue string (typically ", "). + +A request (and response) object have several methods that allow you to retrieve and modify headers. + +* ``getHeaders()``: Get all of the headers of a message as a ``Guzzle\Http\Message\Header\HeaderCollection`` object. +* ``getHeader($header)``: Get a specific header from a message. If the header exists, you'll get a + ``Guzzle\Http\Message\Header`` object. If the header does not exist, this methods returns ``null``. +* ``hasHeader($header)``: Returns true or false based on if the message has a particular header. +* ``setHeader($header, $value)``: Set a header value and overwrite any previously set value for this header. +* ``addHeader($header, $value)``: Add a header with a particular name. If a previous value was already set by the same, + then the header will contain multiple values. +* ``removeHeader($header)``: Remove a header by name from the message. + +.. code-block:: php + + $request = new Request('GET', 'http://httpbin.com/cookies'); + // addHeader will set and append to any existing header values + $request->addHeader('Foo', 'bar'); + $request->addHeader('foo', 'baz'); + // setHeader overwrites any existing values + $request->setHeader('Test', '123'); + + // Request headers can be cast as a string + echo $request->getHeader('Foo'); + // >>> bar, baz + echo $request->getHeader('Test'); + // >>> 123 + + // You can count the number of headers of a particular case insensitive name + echo count($request->getHeader('foO')); + // >>> 2 + + // You can iterate over Header objects + foreach ($request->getHeader('foo') as $header) { + echo $header . "\n"; + } + + // You can get all of the request headers as a Guzzle\Http\Message\Header\HeaderCollection object + $headers = $request->getHeaders(); + + // Missing headers return NULL + var_export($request->getHeader('Missing')); + // >>> null + + // You can see all of the different variations of a header by calling raw() on the Header + var_export($request->getHeader('foo')->raw()); + +Setting the body of a request +----------------------------- + +Requests that can send a body (e.g. PUT, POST, DELETE, PATCH) are instances of +``Guzzle\Http\Message\EntityEnclosingRequestInterface``. Entity enclosing requests contain several methods that allow +you to specify the body to send with a request. + +Use the ``setBody()`` method of a request to set the body that will be sent with a request. This method accepts a +string, a resource returned by ``fopen()``, an array, or an instance of ``Guzzle\Http\EntityBodyInterface``. The body +will then be streamed from the underlying ``EntityBodyInterface`` object owned by the request. When setting the body +of the request, you can optionally specify a Content-Type header and whether or not to force the request to use +chunked Transfer-Encoding. + +.. code-block:: php + + $request = $client->put('/user.json'); + $request->setBody('{"foo":"baz"}', 'application/json'); + +Content-Type header +~~~~~~~~~~~~~~~~~~~ + +Guzzle will automatically add a Content-Type header to a request if the Content-Type can be guessed based on the file +extension of the payload being sent or the file extension present in the path of a request. + +.. code-block:: php + + $request = $client->put('/user.json', array(), '{"foo":"bar"}'); + // The Content-Type was guessed based on the path of the request + echo $request->getHeader('Content-Type'); + // >>> application/json + + $request = $client->put('/user.json'); + $request->setBody(fopen('/tmp/user_data.json', 'r')); + // The Content-Type was guessed based on the path of the entity body + echo $request->getHeader('Content-Type'); + // >>> application/json + +Transfer-Encoding: chunked header +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When sending HTTP requests that contain a payload, you must let the remote server know how to determine when the entire +message has been sent. This usually is done by supplying a ``Content-Length`` header that tells the origin server the +size of the body that is to be sent. In some cases, the size of the payload being sent in a request cannot be known +before initiating the transfer. In these cases (when using HTTP/1.1), you can use the ``Transfer-Encoding: chunked`` +header. + +If the Content-Length cannot be determined (i.e. using a PHP ``http://`` stream), then Guzzle will automatically add +the ``Transfer-Encoding: chunked`` header to the request. + +.. code-block:: php + + $request = $client->put('/user.json'); + $request->setBody(fopen('http://httpbin.org/get', 'r')); + + // The Content-Length could not be determined + echo $request->getHeader('Transfer-Encoding'); + // >>> chunked + +See :doc:`/http-client/entity-bodies` for more information on entity bodies. + +Expect: 100-Continue header +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``Expect: 100-Continue`` header is used to help a client prevent sending a large payload to a server that will +reject the request. This allows clients to fail fast rather than waste bandwidth sending an erroneous payload. Guzzle +will automatically add the ``Expect: 100-Continue`` header to a request when the size of the payload exceeds 1MB or if +the body of the request is not seekable (this helps to prevent errors when a non-seekable body request is redirected). + +.. note:: + + If you find that your larger requests are taking too long to complete, you should first check if the + ``Expect: 100-Continue`` header is being sent with the request. Some servers do not respond well to this header, + which causes cURL to sleep for `1 second `_. + +POST fields and files +~~~~~~~~~~~~~~~~~~~~~ + +Any entity enclosing request can send POST style fields and files. This includes POST, PUT, PATCH, and DELETE requests. +Any request that has set POST fields or files will use cURL's POST message functionality. + +.. code-block:: php + + $request = $client->post('/post'); + // Set an overwrite any previously specified value + $request->setPostField('foo', 'bar'); + // Append a value to any existing values + $request->getPostFields()->add('foo', 'baz'); + // Remove a POST field by name + $request->removePostField('fizz'); + + // Add a file to upload (forces multipart/form-data) + $request->addPostFile('my_file', '/path/to/file', 'plain/text'); + // Remove a POST file by POST key name + $request->removePostFile('my_other_file'); + +.. tip:: + + Adding a large number of POST fields to a POST request is faster if you use the ``addPostFields()`` method so that + you can add and process multiple fields with a single call. Adding multiple POST files is also faster using + ``addPostFiles()``. + +Working with cookies +-------------------- + +Cookies can be modified and retrieved from a request using the following methods: + +.. code-block:: php + + $request->addCookie($name, $value); + $request->removeCookie($name); + $value = $request->getCookie($name); + $valueArray = $request->getCookies(); + +Use the :doc:`cookie plugin ` if you need to reuse cookies between requests. + +.. _request-set-response-body: + +Changing where a response is downloaded +---------------------------------------- + +When a request is sent, the body of the response will be stored in a PHP temp stream by default. You can change the +location in which the response will be downloaded using ``$request->setResponseBody($body)`` or the ``save_to`` request +option. This can be useful for downloading the contents of a URL to a specific file. + +Here's an example of using request options: + +.. code-block:: php + + $request = $this->client->get('http://example.com/large.mov', array(), array( + 'save_to' => '/tmp/large_file.mov' + )); + $request->send(); + var_export(file_exists('/tmp/large_file.mov')); + // >>> true + +Here's an example of using ``setResponseBody()``: + +.. code-block:: php + + $body = fopen('/tmp/large_file.mov', 'w'); + $request = $this->client->get('http://example.com/large.mov'); + $request->setResponseBody($body); + + // You can more easily specify the name of a file to save the contents + // of the response to by passing a string to ``setResponseBody()``. + + $request = $this->client->get('http://example.com/large.mov'); + $request->setResponseBody('/tmp/large_file.mov'); + +Custom cURL options +------------------- + +Most of the functionality implemented in the libcurl bindings has been simplified and abstracted by Guzzle. Developers +who need access to `cURL specific functionality `_ can still add cURL handle +specific behavior to Guzzle HTTP requests by modifying the cURL options collection of a request: + +.. code-block:: php + + $request->getCurlOptions()->set(CURLOPT_LOW_SPEED_LIMIT, 200); + +Other special options that can be set in the ``curl.options`` array include: + ++-------------------------+---------------------------------------------------------------------------------+ +| debug | Adds verbose cURL output to a temp stream owned by the cURL handle object | ++-------------------------+---------------------------------------------------------------------------------+ +| progress | Instructs cURL to emit events when IO events occur. This allows you to be | +| | notified when bytes are transferred over the wire by subscribing to a request's | +| | ``curl.callback.read``, ``curl.callback.write``, and ``curl.callback.progress`` | +| | events. | ++-------------------------+---------------------------------------------------------------------------------+ + +Request options +--------------- + +Requests options can be specified when creating a request or in the ``request.options`` parameter of a client. These +options can control various aspects of a request including: headers to send, query string data, where the response +should be downloaded, proxies, auth, etc. + +.. code-block:: php + + $request = $client->get($url, $headers, array('proxy' => 'http://proxy.com')); + +See :ref:`Request options ` for more information. + +Working with errors +------------------- + +HTTP errors +~~~~~~~~~~~ + +Requests that receive a 4xx or 5xx response will throw a ``Guzzle\Http\Exception\BadResponseException``. More +specifically, 4xx errors throw a ``Guzzle\Http\Exception\ClientErrorResponseException``, and 5xx errors throw a +``Guzzle\Http\Exception\ServerErrorResponseException``. You can catch the specific exceptions or just catch the +BadResponseException to deal with either type of error. Here's an example of catching a generic BadResponseException: + +.. code-block:: php + + try { + $response = $client->get('/not_found.xml')->send(); + } catch (Guzzle\Http\Exception\BadResponseException $e) { + echo 'Uh oh! ' . $e->getMessage(); + echo 'HTTP request URL: ' . $e->getRequest()->getUrl() . "\n"; + echo 'HTTP request: ' . $e->getRequest() . "\n"; + echo 'HTTP response status: ' . $e->getResponse()->getStatusCode() . "\n"; + echo 'HTTP response: ' . $e->getResponse() . "\n"; + } + +Throwing an exception when a 4xx or 5xx response is encountered is the default behavior of Guzzle requests. This +behavior can be overridden by adding an event listener with a higher priority than -255 that stops event propagation. +You can subscribe to ``request.error`` to receive notifications any time an unsuccessful response is received. + +You can change the response that will be associated with the request by calling ``setResponse()`` on the +``$event['request']`` object passed into your listener, or by changing the ``$event['response']`` value of the +``Guzzle\Common\Event`` object that is passed to your listener. Transparently changing the response associated with a +request by modifying the event allows you to retry failed requests without complicating the code that uses the client. +This might be useful for sending requests to a web service that has expiring auth tokens. When a response shows that +your token has expired, you can get a new token, retry the request with the new token, and return the successful +response to the user. + +Here's an example of retrying a request using updated authorization credentials when a 401 response is received, +overriding the response of the original request with the new response, and still allowing the default exception +behavior to be called when other non-200 response status codes are encountered: + +.. code-block:: php + + // Add custom error handling to any request created by this client + $client->getEventDispatcher()->addListener('request.error', function(Event $event) { + + if ($event['response']->getStatusCode() == 401) { + + $newRequest = $event['request']->clone(); + $newRequest->setHeader('X-Auth-Header', MyApplication::getNewAuthToken()); + $newResponse = $newRequest->send(); + + // Set the response object of the request without firing more events + $event['response'] = $newResponse; + + // You can also change the response and fire the normal chain of + // events by calling $event['request']->setResponse($newResponse); + + // Stop other events from firing when you override 401 responses + $event->stopPropagation(); + } + + }); + +cURL errors +~~~~~~~~~~~ + +Connection problems and cURL specific errors can also occur when transferring requests using Guzzle. When Guzzle +encounters cURL specific errors while transferring a single request, a ``Guzzle\Http\Exception\CurlException`` is +thrown with an informative error message and access to the cURL error message. + +A ``Guzzle\Common\Exception\MultiTransferException`` exception is thrown when a cURL specific error occurs while +transferring multiple requests in parallel. You can then iterate over all of the exceptions encountered during the +transfer. + +Plugins and events +------------------ + +Guzzle request objects expose various events that allow you to hook in custom logic. A request object owns a +``Symfony\Component\EventDispatcher\EventDispatcher`` object that can be accessed by calling +``$request->getEventDispatcher()``. You can use the event dispatcher to add listeners (a simple callback function) or +event subscribers (classes that listen to specific events of a dispatcher). You can add event subscribers to a request +directly by just calling ``$request->addSubscriber($mySubscriber);``. + +.. _request-events: + +Events emitted from a request +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A ``Guzzle\Http\Message\Request`` and ``Guzzle\Http\Message\EntityEnclosingRequest`` object emit the following events: + ++------------------------------+--------------------------------------------+------------------------------------------+ +| Event name | Description | Event data | ++==============================+============================================+==========================================+ +| request.before_send | About to send request | * request: Request to be sent | ++------------------------------+--------------------------------------------+------------------------------------------+ +| request.sent | Sent the request | * request: Request that was sent | +| | | * response: Received response | ++------------------------------+--------------------------------------------+------------------------------------------+ +| request.complete | Completed a full HTTP transaction | * request: Request that was sent | +| | | * response: Received response | ++------------------------------+--------------------------------------------+------------------------------------------+ +| request.success | Completed a successful request | * request: Request that was sent | +| | | * response: Received response | ++------------------------------+--------------------------------------------+------------------------------------------+ +| request.error | Completed an unsuccessful request | * request: Request that was sent | +| | | * response: Received response | ++------------------------------+--------------------------------------------+------------------------------------------+ +| request.exception | An unsuccessful response was | * request: Request | +| | received. | * response: Received response | +| | | * exception: BadResponseException | ++------------------------------+--------------------------------------------+------------------------------------------+ +| request.receive.status_line | Received the start of a response | * line: Full response start line | +| | | * status_code: Status code | +| | | * reason_phrase: Reason phrase | +| | | * previous_response: (e.g. redirect) | ++------------------------------+--------------------------------------------+------------------------------------------+ +| curl.callback.progress | cURL progress event (only dispatched when | * handle: CurlHandle | +| | ``emit_io`` is set on a request's curl | * download_size: Total download size | +| | options) | * downloaded: Bytes downloaded | +| | | * upload_size: Total upload bytes | +| | | * uploaded: Bytes uploaded | ++------------------------------+--------------------------------------------+------------------------------------------+ +| curl.callback.write | cURL event called when data is written to | * request: Request | +| | an outgoing stream | * write: Data being written | ++------------------------------+--------------------------------------------+------------------------------------------+ +| curl.callback.read | cURL event called when data is written to | * request: Request | +| | an incoming stream | * read: Data being read | ++------------------------------+--------------------------------------------+------------------------------------------+ + +Creating a request event listener +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Here's an example that listens to the ``request.complete`` event of a request and prints the request and response. + +.. code-block:: php + + use Guzzle\Common\Event; + + $request = $client->get('http://www.google.com'); + + // Echo out the response that was received + $request->getEventDispatcher()->addListener('request.complete', function (Event $e) { + echo $e['request'] . "\n\n"; + echo $e['response']; + }); diff --git a/guzzle/guzzle/docs/http-client/response.rst b/guzzle/guzzle/docs/http-client/response.rst new file mode 100644 index 000000000..ba487316f --- /dev/null +++ b/guzzle/guzzle/docs/http-client/response.rst @@ -0,0 +1,141 @@ +====================== +Using Response objects +====================== + +Sending a request will return a ``Guzzle\Http\Message\Response`` object. You can view the raw HTTP response message by +casting the Response object to a string. Casting the response to a string will return the entity body of the response +as a string too, so this might be an expensive operation if the entity body is stored in a file or network stream. If +you only want to see the response headers, you can call ``getRawHeaders()``. + +Response status line +-------------------- + +The different parts of a response's `status line `_ +(the first line of the response HTTP message) are easily retrievable. + +.. code-block:: php + + $response = $client->get('http://www.amazon.com')->send(); + + echo $response->getStatusCode(); // >>> 200 + echo $response->getReasonPhrase(); // >>> OK + echo $response->getProtocol(); // >>> HTTP + echo $response->getProtocolVersion(); // >>> 1.1 + +You can determine the type of the response using several helper methods: + +.. code-block:: php + + $response->isSuccessful(); // true + $response->isInformational(); + $response->isRedirect(); + $response->isClientError(); + $response->isServerError(); + +Response headers +---------------- + +The Response object contains helper methods for retrieving common response headers. These helper methods normalize the +variations of HTTP response headers. + +.. code-block:: php + + $response->getCacheControl(); + $response->getContentType(); + $response->getContentLength(); + $response->getContentEncoding(); + $response->getContentMd5(); + $response->getEtag(); + // etc... There are methods for every known response header + +You can interact with the Response headers using the same exact methods used to interact with Request headers. See +:ref:`http-message-headers` for more information. + +.. code-block:: php + + echo $response->getHeader('Content-Type'); + echo $response->getHeader('Content-Length'); + echo $response->getHeaders()['Content-Type']; // PHP 5.4 + +Response body +------------- + +The entity body object of a response can be retrieved by calling ``$response->getBody()``. The response EntityBody can +be cast to a string, or you can pass ``true`` to this method to retrieve the body as a string. + +.. code-block:: php + + $request = $client->get('http://www.amazon.com'); + $response = $request->send(); + echo $response->getBody(); + +See :doc:`/http-client/entity-bodies` for more information on entity bodies. + +JSON Responses +~~~~~~~~~~~~~~ + +You can easily parse and use a JSON response as an array using the ``json()`` method of a response. This method will +always return an array if the response is valid JSON or if the response body is empty. You will get an exception if you +call this method and the response is not valid JSON. + +.. code-block:: php + + $data = $response->json(); + echo gettype($data); + // >>> array + +XML Responses +~~~~~~~~~~~~~ + +You can easily parse and use a XML response as SimpleXMLElement object using the ``xml()`` method of a response. This +method will always return a SimpleXMLElement object if the response is valid XML or if the response body is empty. You +will get an exception if you call this method and the response is not valid XML. + +.. code-block:: php + + $xml = $response->xml(); + echo $xml->foo; + // >>> Bar! + +Streaming responses +------------------- + +Some web services provide streaming APIs that allow a client to keep a HTTP request open for an extended period of +time while polling and reading. Guzzle provides a simple way to convert HTTP request messages into +``Guzzle\Stream\Stream`` objects so that you can send the initial headers of a request, read the response headers, and +pull in the response body manually as needed. + +Here's an example using the Twitter Streaming API to track the keyword "bieber": + +.. code-block:: php + + use Guzzle\Http\Client; + use Guzzle\Stream\PhpStreamRequestFactory; + + $client = new Client('https://stream.twitter.com/1'); + + $request = $client->post('statuses/filter.json', null, array( + 'track' => 'bieber' + )); + + $request->setAuth('myusername', 'mypassword'); + + $factory = new PhpStreamRequestFactory(); + $stream = $factory->fromRequest($request); + + // Read until the stream is closed + while (!$stream->feof()) { + // Read a line from the stream + $line = $stream->readLine(); + // JSON decode the line of data + $data = json_decode($line, true); + } + +You can use the ``stream`` request option when using a static client to more easily create a streaming response. + +.. code-block:: php + + $stream = Guzzle::get('http://guzzlephp.org', array('stream' => true)); + while (!$stream->feof()) { + echo $stream->readLine(); + } diff --git a/guzzle/guzzle/docs/http-client/uri-templates.rst b/guzzle/guzzle/docs/http-client/uri-templates.rst new file mode 100644 index 000000000..c18ac3e8d --- /dev/null +++ b/guzzle/guzzle/docs/http-client/uri-templates.rst @@ -0,0 +1,52 @@ +============= +URI templates +============= + +The ``$uri`` passed to one of the client's request creational methods or the base URL of a client can utilize URI +templates. Guzzle supports the entire `URI templates RFC `_. URI templates add a +special syntax to URIs that replace template place holders with user defined variables. + +Every request created by a Guzzle HTTP client passes through a URI template so that URI template expressions are +automatically expanded: + +.. code-block:: php + + $client = new Guzzle\Http\Client('https://example.com/', array('a' => 'hi')); + $request = $client->get('/{a}'); + +Because of URI template expansion, the URL of the above request will become ``https://example.com/hi``. Notice that +the template was expanded using configuration variables of the client. You can pass in custom URI template variables +by passing the URI of your request as an array where the first index of the array is the URI template and the second +index of the array are template variables that are merged into the client's configuration variables. + +.. code-block:: php + + $request = $client->get(array('/test{?a,b}', array('b' => 'there'))); + +The URL for this request will become ``https://test.com?a=hi&b=there``. URI templates aren't limited to just simple +variable replacements; URI templates can provide an enormous amount of flexibility when creating request URIs. + +.. code-block:: php + + $request = $client->get(array('http://example.com{+path}{/segments*}{?query,data*}', array( + 'path' => '/foo/bar', + 'segments' => array('one', 'two'), + 'query' => 'test', + 'data' => array( + 'more' => 'value' + ) + ))); + +The resulting URL would become ``http://example.com/foo/bar/one/two?query=test&more=value``. + +By default, URI template expressions are enclosed in an opening and closing brace (e.g. ``{var}``). If you are working +with a web service that actually uses braces (e.g. Solr), then you can specify a custom regular expression to use to +match URI template expressions. + +.. code-block:: php + + $client->getUriTemplate()->setRegex('/\<\$(.+)\>/'); + $client->get('/<$a>'); + +You can learn about all of the different features of URI templates by reading the +`URI templates RFC `_. diff --git a/guzzle/guzzle/docs/index.rst b/guzzle/guzzle/docs/index.rst new file mode 100644 index 000000000..f76f3bbe6 --- /dev/null +++ b/guzzle/guzzle/docs/index.rst @@ -0,0 +1,5 @@ +.. title:: Guzzle | PHP HTTP client and framework for consuming RESTful web services +.. toctree:: + :hidden: + + docs.rst diff --git a/guzzle/guzzle/docs/iterators/guzzle-iterators.rst b/guzzle/guzzle/docs/iterators/guzzle-iterators.rst new file mode 100644 index 000000000..1c56ec0c5 --- /dev/null +++ b/guzzle/guzzle/docs/iterators/guzzle-iterators.rst @@ -0,0 +1,97 @@ +================ +Guzzle iterators +================ + +Guzzle provides several SPL iterators that can be used with other SPL iterators, including Guzzle resource iterators. +Guzzle's ``guzzle/iterator`` component can also be used independently of the rest of Guzzle through Packagist and +Composer: https://packagist.org/packages/guzzle/iterator + +ChunkedIterator +--------------- + +Pulls out multiple values from an inner iterator and yields and array of values for each outer iteration -- essentially +pulling out chunks of values from the inner iterator. + +.. code-block:: php + + use Guzzle\Iterator\ChunkedIterator; + + $inner = new ArrayIterator(range(0, 8)); + $chunkedIterator = new ChunkedIterator($inner, 10); + + foreach ($chunkedIterator as $chunk) { + echo implode(', ', $chunk) . "\n"; + } + + // >>> 0, 1 + // >>> 2, 3 + // >>> 4, 5 + // >>> 6, 7 + // >>> 8 + +FilterIterator +-------------- + +This iterator is used to filter values out of the inner iterator. This iterator can be used when PHP 5.4's +CallbackFilterIterator is not available. + +.. code-block:: php + + use Guzzle\Iterator\FilterIterator; + + $inner = new ArrayIterator(range(1, 10)); + $filterIterator = new FilterIterator($inner, function ($value) { + return $value % 2; + }); + + foreach ($filterIterator as $value) { + echo $value . "\n"; + } + + // >>> 2 + // >>> 4 + // >>> 6 + // >>> 8 + // >>> 10 + +MapIterator +----------- + +This iterator modifies the values of the inner iterator before yielding. + +.. code-block:: php + + use Guzzle\Iterator\MapIterator; + + $innner = new ArrayIterator(range(0, 3)); + + $mapIterator = new MapIterator($inner, function ($value) { + return $value * 10; + }); + + foreach ($mapIterator as $value) { + echo $value . "\n"; + } + + // >>> 0 + // >>> 10 + // >>> 20 + // >>> 30 + +MethodProxyIterator +------------------- + +This decorator is useful when you need to expose a specific method from an inner iterator that might be wrapper +by one or more iterator decorators. This decorator proxies missing method calls to each inner iterator until one +of the inner iterators can fulfill the call. + +.. code-block:: php + + use Guzzle\Iterator\MethodProxyIterator; + + $inner = new \ArrayIterator(); + $proxy = new MethodProxyIterator($inner); + + // Proxy method calls to the ArrayIterator + $proxy->append('a'); + $proxy->append('b'); diff --git a/guzzle/guzzle/docs/iterators/resource-iterators.rst b/guzzle/guzzle/docs/iterators/resource-iterators.rst new file mode 100644 index 000000000..ce0bee59f --- /dev/null +++ b/guzzle/guzzle/docs/iterators/resource-iterators.rst @@ -0,0 +1,149 @@ +================== +Resource iterators +================== + +Web services often implement pagination in their responses which requires the end-user to issue a series of consecutive +requests in order to fetch all of the data they asked for. Users of your web service client should not be responsible +for implementing the logic involved in iterating through pages of results. Guzzle provides a simple resource iterator +foundation to make it easier on web service client developers to offer a useful abstraction layer. + +Getting an iterator from a client +--------------------------------- + + ResourceIteratorInterface Guzzle\Service\Client::getIterator($command [, array $commandOptions, array $iteratorOptions ]) + +The ``getIterator`` method of a ``Guzzle\Service\ClientInterface`` object provides a convenient interface for +instantiating a resource iterator for a specific command. This method implicitly uses a +``Guzzle\Service\Resource\ResourceIteratorFactoryInterface`` object to create resource iterators. Pass an +instantiated command object or the name of a command in the first argument. When passing the name of a command, the +command factory of the client will create the command by name using the ``$commandOptions`` array. The third argument +may be used to pass an array of options to the constructor of the instantiated ``ResourceIteratorInterface`` object. + +.. code-block:: php + + $iterator = $client->getIterator('get_users'); + + foreach ($iterator as $user) { + echo $user['name'] . ' age ' . $user['age'] . PHP_EOL; + } + +The above code sample might execute a single request or a thousand requests. As a consumer of a web service, I don't +care. I just want to iterate over all of the users. + +Iterator options +~~~~~~~~~~~~~~~~ + +The two universal options that iterators should support are ``limit`` and ``page_size``. Using the ``limit`` option +tells the resource iterator to attempt to limit the total number of iterated resources to a specific amount. Keep in +mind that this is not always possible due to limitations that may be inherent to a web service. The ``page_size`` +option is used to tell a resource iterator how many resources to request per page of results. Much like the ``limit`` +option, you can not rely on getting back exactly the number of resources your specify in the ``page_size`` option. + +.. note:: + + The ``limit`` and ``page_size`` options can also be specified on an iterator using the ``setLimit($limit)`` and + ``setPageSize($pageSize)`` methods. + +Resolving iterator class names +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The default resource iterator factory of a client object expects that your iterators are stored under the ``Model`` +folder of your client and that an iterator is names after the CamelCase name of a command followed by the word +"Iterator". For example, if you wanted to create an iterator for the ``get_users`` command, then your iterator class +would be ``Model\GetUsersIterator`` and would be stored in ``Model/GetUsersIterator.php``. + +Creating an iterator +-------------------- + +While not required, resource iterators in Guzzle typically iterate using a ``Guzzle\Service\Command\CommandInterface`` +object. ``Guzzle\Service\Resource\ResourceIterator``, the default iterator implementation that you should extend, +accepts a command object and array of iterator options in its constructor. The command object passed to the resource +iterator is expected to be ready to execute and not previously executed. The resource iterator keeps a reference of +this command and clones the original command each time a subsequent request needs to be made to fetch more data. + +Implement the sendRequest method +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The most important thing (and usually the only thing) you need to do when creating a resource iterator is to implement +the ``sendRequest()`` method of the resource iterator. The ``sendRequest()`` method is called when you begin +iterating or if there are no resources left to iterate and it you expect to retrieve more resources by making a +subsequent request. The ``$this->command`` property of the resource iterator is updated with a cloned copy of the +original command object passed into the constructor of the iterator. Use this command object to issue your subsequent +requests. + +The ``sendRequest()`` method must return an array of the resources you retrieved from making the subsequent call. +Returning an empty array will stop the iteration. If you suspect that your web service client will occasionally return +an empty result set but still requires further iteration, then you must implement a sort of loop in your +``sendRequest()`` method that will continue to issue subsequent requests until your reach the end of the paginated +result set or until additional resources are retrieved from the web service. + +Update the nextToken property +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Beyond fetching more results, the ``sendRequest()`` method is responsible for updating the ``$this->nextToken`` +property of the iterator. Setting this property to anything other than null tells the iterator that issuing a +subsequent request using the nextToken value will probably return more results. You must continually update this +value in your ``sendRequest()`` method as each response is received from the web service. + +Example iterator +---------------- + +Let's say you want to implement a resource iterator for the ``get_users`` command of your web service. The +``get_users`` command receives a response that contains a list of users, and if there are more pages of results to +retrieve, returns a value called ``next_user``. This return value is known as the **next token** and should be used to +issue subsequent requests. + +Assume the response to a ``get_users`` command returns JSON data that looks like this: + +.. code-block:: javascript + + { + "users": [ + { "name": "Craig Johnson", "age": 10 }, + { "name": "Tom Barker", "age": 20 }, + { "name": "Bob Mitchell", "age": 74 } + ], + "next_user": "Michael Dowling" + } + +Assume that because there is a ``next_user`` value, there will be more users if a subsequent request is issued. If the +``next_user`` value is missing or null, then we know there are no more results to fetch. Let's implement a resource +iterator for this command. + +.. code-block:: php + + namespace MyService\Model; + + use Guzzle\Service\Resource\ResourceIterator; + + /** + * Iterate over a get_users command + */ + class GetUsersIterator extends ResourceIterator + { + protected function sendRequest() + { + // If a next token is set, then add it to the command + if ($this->nextToken) { + $this->command->set('next_user', $this->nextToken); + } + + // Execute the command and parse the result + $result = $this->command->execute(); + + // Parse the next token + $this->nextToken = isset($result['next_user']) ? $result['next_user'] : false; + + return $result['users']; + } + } + +As you can see, it's pretty simple to implement an iterator. There are a few things that you should notice from this +example: + +1. You do not need to create a new command in the ``sendRequest()`` method. A new command object is cloned from the + original command passed into the constructor of the iterator before the ``sendRequest()`` method is called. + Remember that the resource iterator expects a command that has not been executed. +2. When the ``sendRequest()`` method is first called, you will not have a ``$this->nextToken`` value, so always check + before setting it on a command. Notice that the next token is being updated each time a request is sent. +3. After fetching more resources from the service, always return an array of resources. diff --git a/guzzle/guzzle/docs/plugins/async-plugin.rst b/guzzle/guzzle/docs/plugins/async-plugin.rst new file mode 100644 index 000000000..9bd8f4251 --- /dev/null +++ b/guzzle/guzzle/docs/plugins/async-plugin.rst @@ -0,0 +1,18 @@ +============ +Async plugin +============ + +The AsyncPlugin allows you to send requests that do not wait on a response. This is handled through cURL by utilizing +the progress event. When a request has sent all of its data to the remote server, Guzzle adds a 1ms timeout on the +request and instructs cURL to not download the body of the response. The async plugin then catches the exception and +adds a mock response to the request, along with an X-Guzzle-Async header to let you know that the response was not +fully downloaded. + +.. code-block:: php + + use Guzzle\Http\Client; + use Guzzle\Plugin\Async\AsyncPlugin; + + $client = new Client('http://www.example.com'); + $client->addSubscriber(new AsyncPlugin()); + $response = $client->get()->send(); diff --git a/guzzle/guzzle/docs/plugins/backoff-plugin.rst b/guzzle/guzzle/docs/plugins/backoff-plugin.rst new file mode 100644 index 000000000..5a7694141 --- /dev/null +++ b/guzzle/guzzle/docs/plugins/backoff-plugin.rst @@ -0,0 +1,22 @@ +==================== +Backoff retry plugin +==================== + +The ``Guzzle\Plugin\Backoff\BackoffPlugin`` automatically retries failed HTTP requests using custom backoff strategies: + +.. code-block:: php + + use Guzzle\Http\Client; + use Guzzle\Plugin\Backoff\BackoffPlugin; + + $client = new Client('http://www.test.com/'); + // Use a static factory method to get a backoff plugin using the exponential backoff strategy + $backoffPlugin = BackoffPlugin::getExponentialBackoff(); + + // Add the backoff plugin to the client object + $client->addSubscriber($backoffPlugin); + +The BackoffPlugin's constructor accepts a ``Guzzle\Plugin\Backoff\BackoffStrategyInterface`` object that is used to +determine when a retry should be issued and how long to delay between retries. The above code example shows how to +attach a BackoffPlugin to a client that is pre-configured to retry failed 500 and 503 responses using truncated +exponential backoff (emulating the behavior of Guzzle 2's ExponentialBackoffPlugin). diff --git a/guzzle/guzzle/docs/plugins/cache-plugin.rst b/guzzle/guzzle/docs/plugins/cache-plugin.rst new file mode 100644 index 000000000..d2fd5df26 --- /dev/null +++ b/guzzle/guzzle/docs/plugins/cache-plugin.rst @@ -0,0 +1,169 @@ +================= +HTTP Cache plugin +================= + +Guzzle can leverage HTTP's caching specifications using the ``Guzzle\Plugin\Cache\CachePlugin``. The CachePlugin +provides a private transparent proxy cache that caches HTTP responses. The caching logic, based on +`RFC 2616 `_, uses HTTP headers to control caching behavior, +cache lifetime, and supports Vary, ETag, and Last-Modified based revalidation: + +.. code-block:: php + + use Guzzle\Http\Client; + use Doctrine\Common\Cache\FilesystemCache; + use Guzzle\Cache\DoctrineCacheAdapter; + use Guzzle\Plugin\Cache\CachePlugin; + use Guzzle\Plugin\Cache\DefaultCacheStorage; + + $client = new Client('http://www.test.com/'); + + $cachePlugin = new CachePlugin(array( + 'storage' => new DefaultCacheStorage( + new DoctrineCacheAdapter( + new FilesystemCache('/path/to/cache/files') + ) + ) + )); + + // Add the cache plugin to the client object + $client->addSubscriber($cachePlugin); + $client->get('http://www.wikipedia.org/')->send(); + + // The next request will revalidate against the origin server to see if it + // has been modified. If a 304 response is received the response will be + // served from cache + $client->get('http://www.wikipedia.org/')->send(); + +The cache plugin intercepts GET and HEAD requests before they are actually transferred to the origin server. The cache +plugin then generates a hash key based on the request method and URL, and checks to see if a response exists in the cache. If +a response exists in the cache, the cache adapter then checks to make sure that the caching rules associated with the response +satisfy the request, and ensures that response still fresh. If the response is acceptable for the request any required +revalidation, then the cached response is served instead of contacting the origin server. + +Vary +---- + +Cache keys are derived from a request method and a request URL. Multiple responses can map to the same cache key and +stored in Guzzle's underlying cache storage object. You should use the ``Vary`` HTTP header to tell the cache storage +object that the cache response must have been cached for a request that matches the headers specified in the Vary header +of the request. This allows you to have specific cache entries for the same request URL but variations in a request's +headers determine which cache entry is served. Please see the http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.44 +for more information. + +Cache options +------------- + +There are several options you can add to requests or clients to modify the behavior of the cache plugin. + +Override cache TTL +~~~~~~~~~~~~~~~~~~ + +You can override the number of seconds a cacheable response is stored in the cache by setting the +``cache.override_ttl`` parameter on the params object of a request: + +.. code-block:: php + + // If the response to the request is cacheable, then the response will be cached for 100 seconds + $request->getParams()->set('cache.override_ttl', 100); + +If a response doesn't specify any freshness policy, it will be kept in cache for 3600 seconds by default. + +Custom caching decision +~~~~~~~~~~~~~~~~~~~~~~~ + +If the service you are interacting with does not return caching headers or returns responses that are normally +something that would not be cached, you can set a custom ``can_cache`` object on the constructor of the CachePlugin +and provide a ``Guzzle\Plugin\Cache\CanCacheInterface`` object. You can use the +``Guzzle\Plugin\Cache\CallbackCanCacheStrategy`` to easily make a caching decision based on an HTTP request and +response. + +Revalidation options +~~~~~~~~~~~~~~~~~~~~ + +You can change the revalidation behavior of a request using the ``cache.revalidate`` parameter. Setting this +parameter to ``never`` will ensure that a revalidation request is never sent, and the response is always served from +the origin server. Setting this parameter to ``skip`` will never revalidate and uses the response stored in the cache. + +Normalizing requests for caching +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Use the ``cache.key_filter`` parameter if you wish to strip certain query string parameters from your +request before creating a unique hash for the request. This parameter can be useful if your requests have query +string values that cause each request URL to be unique (thus preventing a cache hit). The ``cache.key_filter`` +format is simply a comma separated list of query string values to remove from the URL when creating a cache key. +For example, here we are saying that the ``a`` and ``q`` query string variables should be ignored when generating a +cache key for the request: + +.. code-block:: php + + $request->getParams()->set('cache.key_filter', 'a, q'); + +Other options +~~~~~~~~~~~~~ + +There are many other options available to the CachePlugin that can meet almost any caching requirement, including +custom revalidation implementations, custom cache key generators, custom caching decision strategies, and custom +cache storage objects. Take a look the constructor of ``Guzzle\Plugin\Cache\CachePlugin`` for more information. + +Setting Client-wide cache settings +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can specify cache settings for every request created by a client by adding cache settings to the configuration +options of a client. + +.. code-block:: php + + $client = new Guzzle\Http\Client('http://www.test.com', array( + 'request.params' => array( + 'cache.override_ttl' => 3600, + 'params.cache.revalidate' => 'never' + ) + )); + + echo $client->get('/')->getParams()->get('cache.override_ttl'); + // >>> 3600 + + echo $client->get('/')->getParams()->get('cache.revalidate'); + // >>> never + +Cache revalidation +------------------ + +If the cache plugin determines that a response to a GET request needs revalidation, a conditional GET is transferred +to the origin server. If the origin server returns a 304 response, then a response containing the merged headers of +the cached response with the new response and the entity body of the cached response is returned. Custom revalidation +strategies can be injected into a CachePlugin if needed. + +Cache adapters +-------------- + +Guzzle doesn't try to reinvent the wheel when it comes to caching or logging. Plenty of other frameworks have +excellent solutions in place that you are probably already using in your applications. Guzzle uses adapters for +caching and logging. The cache plugin requires a cache adapter so that is can store responses in a cache. Guzzle +currently supports cache adapters for `Doctrine 2.0 `_ and the +`Zend Framework `_. + +Doctrine cache adapter +~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: php + + use Doctrine\Common\Cache\ArrayCache; + use Guzzle\Cache\DoctrineCacheAdapter; + use Guzzle\Plugin\Cache\CachePlugin; + + $backend = new ArrayCache(); + $adapter = new DoctrineCacheAdapter($backend); + $cache = new CachePlugin($adapter); + +Zend Framework cache adapter +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: php + + use Guzzle\Cache\ZendCacheAdapter; + use Zend\Cache\Backend\TestBackend; + + $backend = new TestBackend(); + $adapter = new ZendCacheAdapter($backend); + $cache = new CachePlugin($adapter); diff --git a/guzzle/guzzle/docs/plugins/cookie-plugin.rst b/guzzle/guzzle/docs/plugins/cookie-plugin.rst new file mode 100644 index 000000000..a6cc7d924 --- /dev/null +++ b/guzzle/guzzle/docs/plugins/cookie-plugin.rst @@ -0,0 +1,33 @@ +============= +Cookie plugin +============= + +Some web services require a Cookie in order to maintain a session. The ``Guzzle\Plugin\Cookie\CookiePlugin`` will add +cookies to requests and parse cookies from responses using a CookieJar object: + +.. code-block:: php + + use Guzzle\Http\Client; + use Guzzle\Plugin\Cookie\CookiePlugin; + use Guzzle\Plugin\Cookie\CookieJar\ArrayCookieJar; + + $cookiePlugin = new CookiePlugin(new ArrayCookieJar()); + + // Add the cookie plugin to a client + $client = new Client('http://www.test.com/'); + $client->addSubscriber($cookiePlugin); + + // Send the request with no cookies and parse the returned cookies + $client->get('http://www.yahoo.com/')->send(); + + // Send the request again, noticing that cookies are being sent + $request = $client->get('http://www.yahoo.com/'); + $request->send(); + + echo $request; + +You can disable cookies per-request by setting the ``cookies.disable`` value to true on a request's params object. + +.. code-block:: php + + $request->getParams()->set('cookies.disable', true); diff --git a/guzzle/guzzle/docs/plugins/creating-plugins.rst b/guzzle/guzzle/docs/plugins/creating-plugins.rst new file mode 100644 index 000000000..0870155b5 --- /dev/null +++ b/guzzle/guzzle/docs/plugins/creating-plugins.rst @@ -0,0 +1,93 @@ +================ +Creating plugins +================ + +.. highlight:: php + +Guzzle is extremely extensible because of the behavioral modifications that can be added to requests, clients, and +commands using an event system. Before and after the majority of actions are taken in the library, an event is emitted +with the name of the event and context surrounding the event. Observers can subscribe to a subject and modify the +subject based on the events received. Guzzle's event system utilizes the Symfony2 EventDispatcher and is the backbone +of its plugin architecture. + +Overview +-------- + +Plugins must implement the ``Symfony\Component\EventDispatcher\EventSubscriberInterface`` interface. The +``EventSubscriberInterface`` requires that your class implements a static method, ``getSubscribedEvents()``, that +returns an associative array mapping events to methods on the object. See the +`Symfony2 documentation `_ for more information. + +Plugins can be attached to any subject, or object in Guzzle that implements that +``Guzzle\Common\HasDispatcherInterface``. + +Subscribing to a subject +~~~~~~~~~~~~~~~~~~~~~~~~ + +You can subscribe an instantiated observer to an event by calling ``addSubscriber`` on a subject. + +.. code-block:: php + + $testPlugin = new TestPlugin(); + $client->addSubscriber($testPlugin); + +You can also subscribe to only specific events using a closure:: + + $client->getEventDispatcher()->addListener('request.create', function(Event $event) { + echo $event->getName(); + echo $event['request']; + }); + +``Guzzle\Common\Event`` objects are passed to notified functions. The Event object has a ``getName()`` method which +return the name of the emitted event and may contain contextual information that can be accessed like an array. + +Knowing what events to listen to +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Any class that implements the ``Guzzle\Common\HasDispatcherInterface`` must implement a static method, +``getAllEvents()``, that returns an array of the events that are emitted from the object. You can browse the source +to see each event, or you can call the static method directly in your code to get a list of available events. + +Event hooks +----------- + +* :ref:`client-events` +* :ref:`service-client-events` +* :ref:`request-events` +* ``Guzzle\Http\Curl\CurlMulti``: +* :ref:`service-builder-events` + +Examples of the event system +---------------------------- + +Simple Echo plugin +~~~~~~~~~~~~~~~~~~ + +This simple plugin prints a string containing the request that is about to be sent by listening to the +``request.before_send`` event:: + + use Symfony\Component\EventDispatcher\EventSubscriberInterface; + + class EchoPlugin implements EventSubscriberInterface + { + public static function getSubscribedEvents() + { + return array('request.before_send' => 'onBeforeSend'); + } + + public function onBeforeSend(Guzzle\Common\Event $event) + { + echo 'About to send a request: ' . $event['request'] . "\n"; + } + } + + $client = new Guzzle\Service\Client('http://www.test.com/'); + + // Create the plugin and add it as an event subscriber + $plugin = new EchoPlugin(); + $client->addSubscriber($plugin); + + // Send a request and notice that the request is printed to the screen + $client->get('/')->send(); + +Running the above code will print a string containing the HTTP request that is about to be sent. diff --git a/guzzle/guzzle/docs/plugins/curl-auth-plugin.rst b/guzzle/guzzle/docs/plugins/curl-auth-plugin.rst new file mode 100644 index 000000000..66d4a01e3 --- /dev/null +++ b/guzzle/guzzle/docs/plugins/curl-auth-plugin.rst @@ -0,0 +1,32 @@ +========================== +cURL authentication plugin +========================== + +.. warning:: + + The CurlAuthPlugin is deprecated. You should use the `auth` parameter of a client to add authorization headers to + every request created by a client. + + .. code-block:: php + + $client->setDefaultOption('auth', array('username', 'password', 'Basic|Digest|NTLM|Any')); + +If your web service client requires basic authorization, then you can use the CurlAuthPlugin to easily add an +Authorization header to each request sent by the client. + +.. code-block:: php + + use Guzzle\Http\Client; + use Guzzle\Plugin\CurlAuth\CurlAuthPlugin; + + $client = new Client('http://www.test.com/'); + + // Add the auth plugin to the client object + $authPlugin = new CurlAuthPlugin('username', 'password'); + $client->addSubscriber($authPlugin); + + $response = $client->get('projects/1/people')->send(); + $xml = new SimpleXMLElement($response->getBody(true)); + foreach ($xml->person as $person) { + echo $person->email . "\n"; + } diff --git a/guzzle/guzzle/docs/plugins/history-plugin.rst b/guzzle/guzzle/docs/plugins/history-plugin.rst new file mode 100644 index 000000000..b96befe79 --- /dev/null +++ b/guzzle/guzzle/docs/plugins/history-plugin.rst @@ -0,0 +1,24 @@ +============== +History plugin +============== + +The history plugin tracks all of the requests and responses sent through a request or client. This plugin can be +useful for crawling or unit testing. By default, the history plugin stores up to 10 requests and responses. + +.. code-block:: php + + use Guzzle\Http\Client; + use Guzzle\Plugin\History\HistoryPlugin; + + $client = new Client('http://www.test.com/'); + + // Add the history plugin to the client object + $history = new HistoryPlugin(); + $history->setLimit(5); + $client->addSubscriber($history); + + $client->get('http://www.yahoo.com/')->send(); + + echo $history->getLastRequest(); + echo $history->getLastResponse(); + echo count($history); diff --git a/guzzle/guzzle/docs/plugins/log-plugin.rst b/guzzle/guzzle/docs/plugins/log-plugin.rst new file mode 100644 index 000000000..3e2b22944 --- /dev/null +++ b/guzzle/guzzle/docs/plugins/log-plugin.rst @@ -0,0 +1,69 @@ +========== +Log plugin +========== + +Use the ``Guzzle\Plugin\Log\LogPlugin`` to view all data sent over the wire, including entity bodies and redirects. + +.. code-block:: php + + use Guzzle\Http\Client; + use Guzzle\Log\Zf1LogAdapter; + use Guzzle\Plugin\Log\LogPlugin; + use Guzzle\Log\MessageFormatter; + + $client = new Client('http://www.test.com/'); + + $adapter = new Zf1LogAdapter( + new \Zend_Log(new \Zend_Log_Writer_Stream('php://output')) + ); + $logPlugin = new LogPlugin($adapter, MessageFormatter::DEBUG_FORMAT); + + // Attach the plugin to the client, which will in turn be attached to all + // requests generated by the client + $client->addSubscriber($logPlugin); + + $response = $client->get('http://google.com')->send(); + +The code sample above wraps a ``Zend_Log`` object using a ``Guzzle\Log\Zf1LogAdapter``. After attaching the plugin to +the client, all data sent over the wire will be logged to stdout. + +The first argument of the LogPlugin's constructor accepts a ``Guzzle\Log\LogAdapterInterface`` object. This object is +an adapter that allows you to use the logging capabilities of your favorite log implementation. The second argument of +the constructor accepts a ``Guzzle\Log\MessageFormatter`` or a log messaged format string. The format string uses +variable substitution and allows you to define the log data that is important to your application. The different +variables that can be injected are as follows: + +================== ==================================================================================== +Variable Substitution +================== ==================================================================================== +{request} Full HTTP request message +{response} Full HTTP response message +{ts} Timestamp +{host} Host of the request +{method} Method of the request +{url} URL of the request +{host} Host of the request +{protocol} Request protocol +{version} Protocol version +{resource} Resource of the request (path + query + fragment) +{port} Port of the request +{hostname} Hostname of the machine that sent the request +{code} Status code of the response (if available) +{phrase} Reason phrase of the response (if available) +{curl_error} Curl error message (if available) +{curl_code} Curl error code (if available) +{curl_stderr} Curl standard error (if available) +{connect_time} Time in seconds it took to establish the connection (if available) +{total_time} Total transaction time in seconds for last transfer (if available) +{req_header_*} Replace `*` with the lowercased name of a request header to add to the message +{res_header_*} Replace `*` with the lowercased name of a response header to add to the message +{req_body} Request body +{res_body} Response body +================== ==================================================================================== + +The LogPlugin has a helper method that can be used when debugging that will output the full HTTP request and +response of a transaction: + +.. code-block:: php + + $client->addSubscriber(LogPlugin::getDebugPlugin()); diff --git a/guzzle/guzzle/docs/plugins/md5-validator-plugin.rst b/guzzle/guzzle/docs/plugins/md5-validator-plugin.rst new file mode 100644 index 000000000..1b1cfa8a8 --- /dev/null +++ b/guzzle/guzzle/docs/plugins/md5-validator-plugin.rst @@ -0,0 +1,29 @@ +==================== +MD5 validator plugin +==================== + +Entity bodies can sometimes be modified over the wire due to a faulty TCP transport or misbehaving proxy. If an HTTP +response contains a Content-MD5 header, then a MD5 hash of the entity body of a response can be compared against the +Content-MD5 header of the response to determine if the response was delivered intact. The +``Guzzle\Plugin\Md5\Md5ValidatorPlugin`` will throw an ``UnexpectedValueException`` if the calculated MD5 hash does +not match the Content-MD5 header value: + +.. code-block:: php + + use Guzzle\Http\Client; + use Guzzle\Plugin\Md5\Md5ValidatorPlugin; + + $client = new Client('http://www.test.com/'); + + $md5Plugin = new Md5ValidatorPlugin(); + + // Add the md5 plugin to the client object + $client->addSubscriber($md5Plugin); + + $request = $client->get('http://www.yahoo.com/'); + $request->send(); + +Calculating the MD5 hash of a large entity body or an entity body that was transferred using a Content-Encoding is an +expensive operation. When working in high performance applications, you might consider skipping the MD5 hash +validation for entity bodies bigger than a certain size or Content-Encoded entity bodies +(see ``Guzzle\Plugin\Md5\Md5ValidatorPlugin`` for more information). diff --git a/guzzle/guzzle/docs/plugins/mock-plugin.rst b/guzzle/guzzle/docs/plugins/mock-plugin.rst new file mode 100644 index 000000000..4900cb565 --- /dev/null +++ b/guzzle/guzzle/docs/plugins/mock-plugin.rst @@ -0,0 +1,27 @@ +=========== +Mock plugin +=========== + +The mock plugin is useful for testing Guzzle clients. The mock plugin allows you to queue an array of responses that +will satisfy requests sent from a client by consuming the request queue in FIFO order. + +.. code-block:: php + + use Guzzle\Http\Client; + use Guzzle\Plugin\Mock\MockPlugin; + use Guzzle\Http\Message\Response; + + $client = new Client('http://www.test.com/'); + + $mock = new MockPlugin(); + $mock->addResponse(new Response(200)) + ->addResponse(new Response(404)); + + // Add the mock plugin to the client object + $client->addSubscriber($mock); + + // The following request will receive a 200 response from the plugin + $client->get('http://www.example.com/')->send(); + + // The following request will receive a 404 response from the plugin + $client->get('http://www.test.com/')->send(); diff --git a/guzzle/guzzle/docs/plugins/oauth-plugin.rst b/guzzle/guzzle/docs/plugins/oauth-plugin.rst new file mode 100644 index 000000000..e67eabaa1 --- /dev/null +++ b/guzzle/guzzle/docs/plugins/oauth-plugin.rst @@ -0,0 +1,30 @@ +============ +OAuth plugin +============ + +Guzzle ships with an OAuth 1.0 plugin that can sign requests using a consumer key, consumer secret, OAuth token, +and OAuth secret. Here's an example showing how to send an authenticated request to the Twitter REST API: + +.. code-block:: php + + use Guzzle\Http\Client; + use Guzzle\Plugin\Oauth\OauthPlugin; + + $client = new Client('http://api.twitter.com/1'); + $oauth = new OauthPlugin(array( + 'consumer_key' => 'my_key', + 'consumer_secret' => 'my_secret', + 'token' => 'my_token', + 'token_secret' => 'my_token_secret' + )); + $client->addSubscriber($oauth); + + $response = $client->get('statuses/public_timeline.json')->send(); + +If you need to use a custom signing method, you can pass a ``signature_method`` configuration option in the +constructor of the OAuth plugin. The ``signature_method`` option must be a callable variable that accepts a string to +sign and signing key and returns a signed string. + +.. note:: + + You can omit the ``token`` and ``token_secret`` options to use two-legged OAuth. diff --git a/guzzle/guzzle/docs/plugins/plugins-list.rst.inc b/guzzle/guzzle/docs/plugins/plugins-list.rst.inc new file mode 100644 index 000000000..8d6d09b46 --- /dev/null +++ b/guzzle/guzzle/docs/plugins/plugins-list.rst.inc @@ -0,0 +1,9 @@ +* :doc:`/plugins/async-plugin` +* :doc:`/plugins/backoff-plugin` +* :doc:`/plugins/cache-plugin` +* :doc:`/plugins/cookie-plugin` +* :doc:`/plugins/history-plugin` +* :doc:`/plugins/log-plugin` +* :doc:`/plugins/md5-validator-plugin` +* :doc:`/plugins/mock-plugin` +* :doc:`/plugins/oauth-plugin` diff --git a/guzzle/guzzle/docs/plugins/plugins-overview.rst b/guzzle/guzzle/docs/plugins/plugins-overview.rst new file mode 100644 index 000000000..19ae57ece --- /dev/null +++ b/guzzle/guzzle/docs/plugins/plugins-overview.rst @@ -0,0 +1,59 @@ +====================== +Plugin system overview +====================== + +The workflow of sending a request and parsing a response is driven by Guzzle's event system, which is powered by the +`Symfony2 Event Dispatcher component `_. + +Any object in Guzzle that emits events will implement the ``Guzzle\Common\HasEventDispatcher`` interface. You can add +event subscribers directly to these objects using the ``addSubscriber()`` method, or you can grab the +``Symfony\Component\EventDispatcher\EventDispatcher`` object owned by the object using ``getEventDispatcher()`` and +add a listener or event subscriber. + +Adding event subscribers to clients +----------------------------------- + +Any event subscriber or event listener attached to the EventDispatcher of a ``Guzzle\Http\Client`` or +``Guzzle\Service\Client`` object will automatically be attached to all request objects created by the client. This +allows you to attach, for example, a HistoryPlugin to a client object, and from that point on, every request sent +through that client will utilize the HistoryPlugin. + +.. code-block:: php + + use Guzzle\Plugin\History\HistoryPlugin; + use Guzzle\Service\Client; + + $client = new Client(); + + // Create a history plugin and attach it to the client + $history = new HistoryPlugin(); + $client->addSubscriber($history); + + // Create and send a request. This request will also utilize the HistoryPlugin + $client->get('http://httpbin.org')->send(); + + // Echo out the last sent request by the client + echo $history->getLastRequest(); + +.. tip:: + + :doc:`Create event subscribers `, or *plugins*, to implement reusable logic that can be + shared across clients. Event subscribers are also easier to test than anonymous functions. + +Pre-Built plugins +----------------- + +Guzzle provides easy to use request plugins that add behavior to requests based on signal slot event notifications +powered by the Symfony2 Event Dispatcher component. + +* :doc:`async-plugin` +* :doc:`backoff-plugin` +* :doc:`cache-plugin` +* :doc:`cookie-plugin` +* :doc:`curl-auth-plugin` +* :doc:`history-plugin` +* :doc:`log-plugin` +* :doc:`md5-validator-plugin` +* :doc:`mock-plugin` +* :doc:`oauth-plugin` + diff --git a/guzzle/guzzle/docs/requirements.txt b/guzzle/guzzle/docs/requirements.txt new file mode 100644 index 000000000..f62e31837 --- /dev/null +++ b/guzzle/guzzle/docs/requirements.txt @@ -0,0 +1,2 @@ +Sphinx>=1.2b1 +guzzle_sphinx_theme>=0.5.0 diff --git a/guzzle/guzzle/docs/testing/unit-testing.rst b/guzzle/guzzle/docs/testing/unit-testing.rst new file mode 100644 index 000000000..f4297af30 --- /dev/null +++ b/guzzle/guzzle/docs/testing/unit-testing.rst @@ -0,0 +1,201 @@ +=========================== +Unit Testing Guzzle clients +=========================== + +Guzzle provides several tools that will enable you to easily unit test your web service clients. + +* PHPUnit integration +* Mock responses +* node.js web server for integration testing + +PHPUnit integration +------------------- + +Guzzle is unit tested using `PHPUnit `_. Your web service client's unit tests should extend +``Guzzle\Tests\GuzzleTestCase`` so that you can take advantage of some of the built in helpers. + +In order to unit test your client, a developer would need to copy phpunit.xml.dist to phpunit.xml and make any needed +modifications. As a best practice and security measure for you and your contributors, it is recommended to add an +ignore statement to your SCM so that phpunit.xml is ignored. + +Bootstrapping +~~~~~~~~~~~~~ + +Your web service client should have a tests/ folder that contains a bootstrap.php file. The bootstrap.php file +responsible for autoloading and configuring a ``Guzzle\Service\Builder\ServiceBuilder`` that is used throughout your +unit tests for loading a configured client. You can add custom parameters to your phpunit.xml file that expects users +to provide the path to their configuration data. + +.. code-block:: php + + Guzzle\Tests\GuzzleTestCase::setServiceBuilder(Aws\Common\Aws::factory($_SERVER['CONFIG'])); + + Guzzle\Tests\GuzzleTestCase::setServiceBuilder(Guzzle\Service\Builder\ServiceBuilder::factory(array( + 'test.unfuddle' => array( + 'class' => 'Guzzle.Unfuddle.UnfuddleClient', + 'params' => array( + 'username' => 'test_user', + 'password' => '****', + 'subdomain' => 'test' + ) + ) + ))); + +The above code registers a service builder that can be used throughout your unit tests. You would then be able to +retrieve an instantiated and configured Unfuddle client by calling ``$this->getServiceBuilder()->get('test.unfuddle)``. +The above code assumes that ``$_SERVER['CONFIG']`` contains the path to a file that stores service description +configuration. + +Unit testing remote APIs +------------------------ + +Mock responses +~~~~~~~~~~~~~~ + +One of the benefits of unit testing is the ability to quickly determine if there are errors in your code. If your +unit tests run slowly, then they become tedious and will likely be run less frequently. Guzzle's philosophy on unit +testing web service clients is that no network access should be required to run the unit tests. This means that +responses are served from mock responses or local servers. By adhering to this principle, tests will run much faster +and will not require an external resource to be available. The problem with this approach is that your mock responses +must first be gathered and then subsequently updated each time the remote API changes. + +Integration testing over the internet +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can perform integration testing with a web service over the internet by making calls directly to the service. If +the web service you are requesting uses a complex signing algorithm or some other specific implementation, then you +may want to include at least one actual network test that can be run specifically through the command line using +`PHPUnit group annotations `_. + +@group internet annotation +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When creating tests that require an internet connection, it is recommended that you add ``@group internet`` annotations +to your unit tests to specify which tests require network connectivity. + +You can then `run PHPUnit tests `_ that exclude the @internet +group by running ``phpunit --exclude-group internet``. + +API credentials +^^^^^^^^^^^^^^^ + +If API credentials are required to run your integration tests, you must add ```` parameters to your +phpunit.xml.dist file and extract these parameters in your bootstrap.php file. + +.. code-block:: xml + + + + + + + + + + + + + ./Tests + + + + +You can then extract the ``server`` variables in your bootstrap.php file by grabbing them from the ``$_SERVER`` +superglobal: ``$apiUser = $_SERVER['API_USER'];`` + +Further reading +^^^^^^^^^^^^^^^ + +A good discussion on the topic of testing remote APIs can be found in Sebastian Bergmann's +`Real-World Solutions for Developing High-Quality PHP Frameworks and Applications `_. + +Queueing Mock responses +----------------------- + +Mock responses can be used to test if requests are being generated correctly and responses and handled correctly by +your client. Mock responses can be queued up for a client using the ``$this->setMockResponse($client, $path)`` method +of your test class. Pass the client you are adding mock responses to and a single path or array of paths to mock +response files relative to the ``/tests/mock/ folder``. This will queue one or more mock responses for your client by +creating a simple observer on the client. Mock response files must contain a full HTTP response message: + +.. code-block:: none + + HTTP/1.1 200 OK + Date: Wed, 25 Nov 2009 12:00:00 GMT + Connection: close + Server: AmazonS3 + Content-Type: application/xml + + + EU + +After queuing mock responses for a client, you can get an array of the requests that were sent by the client that +were issued a mock response by calling ``$this->getMockedRequests()``. + +You can also use the ``Guzzle\Plugin\Mock\MockPlugin`` object directly with your clients. + +.. code-block:: php + + $plugin = new Guzzle\Plugin\Mock\MockPlugin(); + $plugin->addResponse(new Guzzle\Http\Message\Response(200)); + $client = new Guzzle\Http\Client(); + $client->addSubscriber($plugin); + + // The following request will get the mock response from the plugin in FIFO order + $request = $client->get('http://www.test.com/'); + $request->send(); + + // The MockPlugin maintains a list of requests that were mocked + $this->assertContainsOnly($request, $plugin->getReceivedRequests()); + +node.js web server for integration testing +------------------------------------------ + +Using mock responses is usually enough when testing a web service client. If your client needs to add custom cURL +options to requests, then you should use the node.js test web server to ensure that your HTTP request message is +being created correctly. + +Guzzle is based around PHP's libcurl bindings. cURL sometimes modifies an HTTP request message based on +``CURLOPT_*`` options. Headers that are added to your request by cURL will not be accounted for if you inject mock +responses into your tests. Additionally, some request entity bodies cannot be loaded by the client before transmitting +it to the sever (for example, when using a client as a sort of proxy and streaming content from a remote server). You +might also need to inspect the entity body of a ``multipart/form-data`` POST request. + +.. note:: + + You can skip all of the tests that require the node.js test web server by excluding the ``server`` group: + ``phpunit --exclude-group server`` + +Using the test server +~~~~~~~~~~~~~~~~~~~~~ + +The node.js test server receives requests and returns queued responses. The test server exposes a simple API that is +used to enqueue responses and inspect the requests that it has received. + +Retrieve the server object by calling ``$this->getServer()``. If the node.js server is not running, it will be +started as a forked process and an object that interfaces with the server will be returned. (note: stopping the +server is handled internally by Guzzle.) + +You can queue an HTTP response or an array of responses by calling ``$this->getServer()->enqueue()``: + +.. code-block:: php + + $this->getServer()->enqueue("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); + +The above code queues a single 200 response with an empty body. Responses are queued using a FIFO order; this +response will be returned by the server when it receives the first request and then removed from the queue. If a +request is received by a server with no queued responses, an exception will be thrown in your unit test. + +You can inspect the requests that the server has retrieved by calling ``$this->getServer()->getReceivedRequests()``. +This method accepts an optional ``$hydrate`` parameter that specifies if you are retrieving an array of string HTTP +requests or an array of ``Guzzle\Http\RequestInterface`` subclassed objects. "Hydrating" the requests will allow +greater flexibility in your unit tests so that you can easily assert the state of the various parts of a request. + +You will need to modify the base_url of your web service client in order to use it against the test server. + +.. code-block:: php + + $client = $this->getServiceBuilder()->get('my_client'); + $client->setBaseUrl($this->getServer()->getUrl()); + +After running the above code, all calls made from the ``$client`` object will be sent to the test web server. diff --git a/guzzle/guzzle/docs/webservice-client/guzzle-service-descriptions.rst b/guzzle/guzzle/docs/webservice-client/guzzle-service-descriptions.rst new file mode 100644 index 000000000..b1901fc7d --- /dev/null +++ b/guzzle/guzzle/docs/webservice-client/guzzle-service-descriptions.rst @@ -0,0 +1,619 @@ +=========================== +Guzzle service descriptions +=========================== + +Guzzle allows you to serialize HTTP requests and parse HTTP responses using a DSL called a service descriptions. +Service descriptions define web service APIs by documenting each operation, the operation's parameters, validation +options for each parameter, an operation's response, how the response is parsed, and any errors that can be raised for +an operation. Writing a service description for a web service allows you to more quickly consume a web service than +writing concrete commands for each web service operation. + +Guzzle service descriptions can be representing using a PHP array or JSON document. Guzzle's service descriptions are +heavily inspired by `Swagger `_. + +Service description schema +========================== + +A Guzzle Service description must match the following JSON schema document. This document can also serve as a guide when +implementing a Guzzle service description. + +Download the schema here: :download:`Guzzle JSON schema document ` + +.. class:: overflow-height-500px + + .. literalinclude:: ../_downloads/guzzle-schema-1.0.json + :language: json + +Top-level attributes +-------------------- + +Service descriptions are comprised of the following top-level attributes: + +.. code-block:: json + + { + "name": "string", + "apiVersion": "string|number", + "baseUrl": "string", + "description": "string", + "operations": {}, + "models": {}, + "includes": ["string.php", "string.json"] + } + ++-----------------------------------------+-------------------------+-----------------------------------------------------------------------------------------------------------------------+ +| Property Name | Value | Description | ++=========================================+=========================+=======================================================================================================================+ +| name | string | Name of the web service | ++-----------------------------------------+-------------------------+-----------------------------------------------------------------------------------------------------------------------+ +| apiVersion | string|number | Version identifier that the service description is compatible with | ++-----------------------------------------+-------------------------+-----------------------------------------------------------------------------------------------------------------------+ +| baseUrl or basePath | string | Base URL of the web service. Any relative URI specified in an operation will be merged with the baseUrl using the | +| | | process defined in RFC 2396. Some clients require custom logic to determine the baseUrl. In those cases, it is best | +| | | to not include a baseUrl in the service description, but rather allow the factory method of the client to configure | +| | | the client’s baseUrl. | ++-----------------------------------------+-------------------------+-----------------------------------------------------------------------------------------------------------------------+ +| description | string | Short summary of the web service | ++-----------------------------------------+-------------------------+-----------------------------------------------------------------------------------------------------------------------+ +| operations | object containing | Operations of the service. The key is the name of the operation and value is the attributes of the operation. | +| | :ref:`operation-schema` | | +| | | | ++-----------------------------------------+-------------------------+-----------------------------------------------------------------------------------------------------------------------+ +| models | object containing | Schema models that can be referenced throughout the service description. Models can be used to define how an HTTP | +| | :ref:`model-schema` | response is parsed into a ``Guzzle\Service\Resource\Model`` object when an operation uses a ``model`` ``responseType``| ++-----------------------------------------+-------------------------+-----------------------------------------------------------------------------------------------------------------------+ +| includes | array of .js, | Service description files to include and extend from (can be a .json, .js, or .php file) | +| | .json, or .php | | +| | files. | | ++-----------------------------------------+-------------------------+-----------------------------------------------------------------------------------------------------------------------+ +| (any additional properties) | mixed | Any additional properties specified as top-level attributes are allowed and will be treated as arbitrary data | ++-----------------------------------------+-------------------------+-----------------------------------------------------------------------------------------------------------------------+ + +.. _operation-schema: + +Operations +---------- + +Operations are the actions that can be taken on a service. Each operation is given a unique name and has a distinct +endpoint and HTTP method. If an API has a ``DELETE /users/:id`` operation, a satisfactory operation name might be +``DeleteUser`` with a parameter of ``id`` that is inserted into the URI. + +.. class:: overflow-height-250px + + .. code-block:: json + + { + "operations": { + "operationName": { + "extends": "string", + "httpMethod": "GET|POST|PUT|DELETE|PATCH|string", + "uri": "string", + "summary": "string", + "class": "string", + "responseClass": "string", + "responseNotes": "string", + "type": "string", + "description": "string", + "responseType": "primitive|class|(model by name)|documentation|(string)", + "deprecated": false, + "errorResponses": [ + { + "code": 500, + "phrase": "Unexpected Error", + "class": "string" + } + ], + "data": { + "foo": "bar", + "baz": "bam" + }, + "parameters": {} + } + } + } + +.. csv-table:: + :header: "Property Name", "Value", "Description" + :widths: 20, 15, 65 + + "extends", "string", "Extend from another operation by name. The parent operation must be defined before the child." + "httpMethod", "string", "HTTP method used with the operation (e.g. GET, POST, PUT, DELETE, PATCH, etc)" + "uri", "string", "URI of the operation. The uri attribute can contain URI templates. The variables of the URI template are parameters of the operation with a location value of uri" + "summary", "string", "Short summary of what the operation does" + "class", "string", "Custom class to instantiate instead of the default Guzzle\\Service\\Command\\OperationCommand. Using this attribute allows you to define an operation using a service description, but allows more customized logic to be implemented in user-land code." + "responseClass", "string", "Defined what is returned from the method. Can be a primitive, class name, or model name. You can specify the name of a class to return a more customized result from the operation (for example, a domain model object). When using the name of a PHP class, the class must implement ``Guzzle\Service\Command\ResponseClassInterface``." + "responseNotes", "string", "A description of the response returned by the operation" + "responseType", "string", "The type of response that the operation creates: one of primitive, class, model, or documentation. If not specified, this value will be automatically inferred based on whether or not there is a model matching the name, if a matching class name is found, or set to 'primitive' by default." + "deprecated", "boolean", "Whether or not the operation is deprecated" + "errorResponses", "array", "Errors that could occur while executing the operation. Each item of the array is an object that can contain a 'code' (HTTP response status code of the error), 'phrase' (reason phrase or description of the error), and 'class' (an exception class that will be raised when this error is encountered)" + "data", "object", "Any arbitrary data to associate with the operation" + "parameters", "object containing :ref:`parameter-schema` objects", "Parameters of the operation. Parameters are used to define how input data is serialized into a HTTP request." + "additionalParameters", "A single :ref:`parameter-schema` object", "Validation and serialization rules for any parameter supplied to the operation that was not explicitly defined." + +additionalParameters +~~~~~~~~~~~~~~~~~~~~ + +When a webservice offers a large number of parameters that all are set in the same location (for example the query +string or a JSON document), defining each parameter individually can require a lot of time and repetition. Furthermore, +some web services allow for completely arbitrary parameters to be supplied for an operation. The +``additionalParameters`` attribute can be used to solve both of these issues. + +As an example, we can define a Twitter API operation quite easily using ``additionalParameters``. The +GetMentions operation accepts a large number of query string parameters. Defining each of these parameters +is ideal because it provide much more introspection for the client and opens the possibility to use the description with +other tools (e.g. a documentation generator). However, you can very quickly provide a "catch-all" serialization rule +that will place any custom parameters supplied to an operation the generated request's query string parameters. + +.. class:: overflow-height-250px + + .. code-block:: json + + { + "name": "Twitter", + "apiVersion": "1.1", + "baseUrl": "https://api.twitter.com/1.1", + "operations": { + "GetMentions": { + "httpMethod": "GET", + "uri": "statuses/mentions_timeline.json", + "responseClass": "GetMentionsOutput", + "additionalParameters": { + "location": "query" + } + } + }, + "models": { + "GetMentionsOutput": { + "type": "object", + "additionalProperties": { + "location": "json" + } + } + } + } + +responseClass +~~~~~~~~~~~~~ + +The ``responseClass`` attribute is used to define the return value of an operation (what is returned by calling the +``getResult()`` method of a command object). The value set in the responseClass attribute can be one of "primitive" +(meaning the result with be primitive type like a string), a class name meaning the result will be an instance of a +specific user-land class, or a model name meaning the result will be a ``Guzzle\Service\Resource\Model`` object that +uses a :ref:`model schema ` to define how the HTTP response is parsed. + +.. note:: + + Using a class name with a ``responseClass`` will only work if it is supported by the ``class`` that is instantiated + for the operation. Keep this in mind when specifying a custom ``class`` attribute that points to a custom + ``Guzzle\Service\Command\CommandInterface`` class. The default ``class``, + ``Guzzle\Service\Command\OperationCommand``, does support setting custom ``class`` attributes. + +You can specify the name of a class to return a more customized result from the operation (for example, a domain model +object). When using the name of a PHP class, the class must implement ``Guzzle\Service\Command\ResponseClassInterface``. +Here's a very simple example of implementing a custom responseClass object. + +.. code-block:: json + + { + "operations": { + "test": { + "responseClass": "MyApplication\\User" + } + } + } + +.. code-block:: php + + namespace MyApplication; + + use Guzzle\Service\Command\ResponseClassInterface; + use Guzzle\Service\Command\OperationCommand; + + class User implements ResponseClassInterface + { + protected $name; + + public static function fromCommand(OperationCommand $command) + { + $response = $command->getResponse(); + $xml = $command->xml(); + + return new self((string) $xml->name); + } + + public function __construct($name) + { + $this->name = $name; + } + } + +errorResponses +~~~~~~~~~~~~~~ + +``errorResponses`` is an array containing objects that define the errors that could occur while executing the +operation. Each item of the array is an object that can contain a 'code' (HTTP response status code of the error), +'phrase' (reason phrase or description of the error), and 'class' (an exception class that will be raised when this +error is encountered). + +ErrorResponsePlugin +^^^^^^^^^^^^^^^^^^^ + +Error responses are by default only used for documentation. If you don't need very complex exception logic for your web +service errors, then you can use the ``Guzzle\Plugin\ErrorResponse\ErrorResponsePlugin`` to automatically throw defined +exceptions when one of the ``errorResponse`` rules are matched. The error response plugin will listen for the +``request.complete`` event of a request created by a command object. Every response (including a successful response) is +checked against the list of error responses for an exact match using the following order of checks: + +1. Does the errorResponse have a defined ``class``? +2. Is the errorResponse ``code`` equal to the status code of the response? +3. Is the errorResponse ``phrase`` equal to the reason phrase of the response? +4. Throw the exception stored in the ``class`` attribute of the errorResponse. + +The ``class`` attribute must point to a class that implements +``Guzzle\Plugin\ErrorResponse\ErrorResponseExceptionInterface``. This interface requires that an error response class +implements ``public static function fromCommand(CommandInterface $command, Response $response)``. This method must +return an object that extends from ``\Exception``. After an exception is returned, it is thrown by the plugin. + +.. _parameter-schema: + +Parameter schema +---------------- + +Parameters in both operations and models are represented using the +`JSON schema `_ syntax. + +.. csv-table:: + :header: "Property Name", "Value", "Description" + :widths: 20, 15, 65 + + "name", "string", "Unique name of the parameter" + "type", "string|array", "Type of variable (string, number, integer, boolean, object, array, numeric, null, any). Types are using for validation and determining the structure of a parameter. You can use a union type by providing an array of simple types. If one of the union types matches the provided value, then the value is valid." + "instanceOf", "string", "When the type is an object, you can specify the class that the object must implement" + "required", "boolean", "Whether or not the parameter is required" + "default", "mixed", "Default value to use if no value is supplied" + "static", "boolean", "Set to true to specify that the parameter value cannot be changed from the default setting" + "description", "string", "Documentation of the parameter" + "location", "string", "The location of a request used to apply a parameter. Custom locations can be registered with a command, but the defaults are uri, query, statusCode, reasonPhrase, header, body, json, xml, postField, postFile, responseBody" + "sentAs", "string", "Specifies how the data being modeled is sent over the wire. For example, you may wish to include certain headers in a response model that have a normalized casing of FooBar, but the actual header is x-foo-bar. In this case, sentAs would be set to x-foo-bar." + "filters", "array", "Array of functions to to run a parameter value through." + +filters +~~~~~~~ + +Each value in the array must be a string containing the full class path to a static method or an array of complex +filter information. You can specify static methods of classes using the full namespace class name followed by +"::" (e.g. ``FooBar::baz()``). Some filters require arguments in order to properly filter a value. For complex filters, +use an object containing a ``method`` attribute pointing to a function, and an ``args`` attribute containing an +array of positional arguments to pass to the function. Arguments can contain keywords that are replaced when filtering +a value: ``@value`` is replaced with the value being filtered, and ``@api`` is replaced with the actual Parameter +object. + +.. code-block:: json + + { + "filters": [ + "strtolower", + { + "method": "MyClass::convertString", + "args": [ "test", "@value", "@api" ] + } + ] + } + +The above example will filter a parameter using ``strtolower``. It will then call the ``convertString`` static method +of ``MyClass``, passing in "test", the actual value of the parameter, and a ``Guzzle\Service\Description\Parameter`` +object. + +Operation parameter location attributes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The location field of top-level parameters control how a parameter is serialized when generating a request. + +uri location +^^^^^^^^^^^^ + +Parameters are injected into the ``uri`` attribute of the operation using +`URI-template expansion `_. + +.. code-block:: json + + { + "operations": { + "uriTest": { + "uri": "/test/{testValue}", + "parameters": { + "testValue": { + "location": "uri" + } + } + } + } + } + +query location +^^^^^^^^^^^^^^ + +Parameters are injected into the query string of a request. Query values can be nested, which would result in a PHP +style nested query string. The name of a parameter is the default name of the query string parameter added to the +request. You can override this behavior by specifying the ``sentAs`` attribute on the parameter. + +.. code-block:: json + + { + "operations": { + "queryTest": { + "parameters": { + "testValue": { + "location": "query", + "sentAs": "test_value" + } + } + } + } + } + +header location +^^^^^^^^^^^^^^^ + +Parameters are injected as headers on an HTTP request. The name of the parameter is used as the name of the header by +default. You can change the name of the header created by the parameter using the ``sentAs`` attribute. + +Headers that are of type ``object`` will be added as multiple headers to a request using the key of the input array as +the header key. Setting a ``sentAs`` attribute along with a type ``object`` will use the value of ``sentAs`` as a +prefix for each header key. + +body location +^^^^^^^^^^^^^ + +Parameters are injected as the body of a request. The input of these parameters may be anything that can be cast to a +string or a ``Guzzle\Http\EntityBodyInterface`` object. + +postField location +^^^^^^^^^^^^^^^^^^ + +Parameters are inserted as POST fields in a request. Nested values may be supplied and will be represented using +PHP style nested query strings. The POST field name is the same as the parameter name by default. You can use the +``sentAs`` parameter to override the POST field name. + +postFile location +^^^^^^^^^^^^^^^^^ + +Parameters are added as POST files. A postFile value may be a string pointing to a local filename or a +``Guzzle\Http\Message\PostFileInterface`` object. The name of the POST file will be the name of the parameter by +default. You can use a custom POST file name by using the ``sentAs`` attribute. + +Supports "string" and "array" types. + +json location +^^^^^^^^^^^^^ + +Parameters are added to the body of a request as top level keys of a JSON document. Nested values may be specified, +with any number of nested ``Guzzle\Common\ToArrayInterface`` objects. When JSON parameters are specified, the +``Content-Type`` of the request will change to ``application/json`` if a ``Content-Type`` has not already been specified +on the request. + +xml location +^^^^^^^^^^^^ + +Parameters are added to the body of a request as top level nodes of an XML document. Nested values may be specified, +with any number of nested ``Guzzle\Common\ToArrayInterface`` objects. When XML parameters are specified, the +``Content-Type`` of the request will change to ``application/xml`` if a ``Content-Type`` has not already been specified +on the request. + +responseBody location +^^^^^^^^^^^^^^^^^^^^^ + +Specifies the EntityBody of a response. This can be used to download the response body to a file or a custom Guzzle +EntityBody object. + +No location +^^^^^^^^^^^ + +If a parameter has no location attribute, then the parameter is simply used as a data value. + +Other locations +^^^^^^^^^^^^^^^ + +Custom locations can be registered as new locations or override default locations if needed. + +.. _model-schema: + +Model Schema +------------ + +Models are used in service descriptions to provide generic JSON schema definitions that can be extended from or used in +``$ref`` attributes. Models can also be referenced in a ``responseClass`` attribute to provide valuable output to an +operation. Models are JSON schema documents and use the exact syntax and attributes used in parameters. + +Response Models +~~~~~~~~~~~~~~~ + +Response models describe how a response is parsed into a ``Guzzle\Service\Resource\Model`` object. Response models are +always modeled as JSON schema objects. When an HTTP response is parsed using a response model, the rules specified on +each property of a response model will translate 1:1 as keys in a PHP associative array. When a ``sentAs`` attribute is +found in response model parameters, the value retrieved from the HTTP response is retrieved using the ``sentAs`` +parameter but stored in the response model using the name of the parameter. + +The location field of top-level parameters in a response model tell response parsers how data is retrieved from a +response. + +statusCode location +^^^^^^^^^^^^^^^^^^^ + +Retrieves the status code of the response. + +reasonPhrase location +^^^^^^^^^^^^^^^^^^^^^ + +Retrieves the reason phrase of the response. + +header location +^^^^^^^^^^^^^^^ + +Retrieves a header from the HTTP response. + +body location +^^^^^^^^^^^^^ + +Retrieves the body of an HTTP response. + +json location +^^^^^^^^^^^^^ + +Retrieves a top-level parameter from a JSON document contained in an HTTP response. + +You can use ``additionalProperties`` if the JSON document is wrapped in an outer array. This allows you to parse the +contents of each item in the array using the parsing rules defined in the ``additionalProperties`` schema. + +xml location +^^^^^^^^^^^^ + +Retrieves a top-level node value from an XML document contained in an HTTP response. + +Other locations +^^^^^^^^^^^^^^^ + +Custom locations can be registered as new locations or override default locations if needed. + +Example service description +--------------------------- + +Let's say you're interacting with a web service called 'Foo' that allows for the following routes and methods:: + + GET/POST /users + GET/DELETE /users/:id + +The following JSON service description implements this simple web service: + +.. class:: overflow-height-500px + + .. code-block:: json + + { + "name": "Foo", + "apiVersion": "2012-10-14", + "baseUrl": "http://api.foo.com", + "description": "Foo is an API that allows you to Baz Bar", + "operations": { + "GetUsers": { + "httpMethod": "GET", + "uri": "/users", + "summary": "Gets a list of users", + "responseClass": "GetUsersOutput" + }, + "CreateUser": { + "httpMethod": "POST", + "uri": "/users", + "summary": "Creates a new user", + "responseClass": "CreateUserOutput", + "parameters": { + "name": { + "location": "json", + "type": "string" + }, + "age": { + "location": "json", + "type": "integer" + } + } + }, + "GetUser": { + "httpMethod": "GET", + "uri": "/users/{id}", + "summary": "Retrieves a single user", + "responseClass": "GetUserOutput", + "parameters": { + "id": { + "location": "uri", + "description": "User to retrieve by ID", + "required": true + } + } + }, + "DeleteUser": { + "httpMethod": "DELETE", + "uri": "/users/{id}", + "summary": "Deletes a user", + "responseClass": "DeleteUserOutput", + "parameters": { + "id": { + "location": "uri", + "description": "User to delete by ID", + "required": true + } + } + } + }, + "models": { + "GetUsersOutput": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "location": "json", + "type": "string" + }, + "age": { + "location": "json", + "type": "integer" + } + } + } + }, + "CreateUserOutput": { + "type": "object", + "properties": { + "id": { + "location": "json", + "type": "string" + }, + "location": { + "location": "header", + "sentAs": "Location", + "type": "string" + } + } + }, + "GetUserOutput": { + "type": "object", + "properties": { + "name": { + "location": "json", + "type": "string" + }, + "age": { + "location": "json", + "type": "integer" + } + } + }, + "DeleteUserOutput": { + "type": "object", + "properties": { + "status": { + "location": "statusCode", + "type": "integer" + } + } + } + } + } + +If you attach this service description to a client, you would completely configure the client to interact with the +Foo web service and provide valuable response models for each operation. + +.. code-block:: php + + use Guzzle\Service\Description\ServiceDescription; + + $description = ServiceDescription::factory('/path/to/client.json'); + $client->setDescription($description); + + $command = $client->getCommand('DeleteUser', array('id' => 123)); + $responseModel = $client->execute($command); + echo $responseModel['status']; + +.. note:: + + You can add the service description to your client's factory method or constructor. diff --git a/guzzle/guzzle/docs/webservice-client/using-the-service-builder.rst b/guzzle/guzzle/docs/webservice-client/using-the-service-builder.rst new file mode 100644 index 000000000..b7113d68b --- /dev/null +++ b/guzzle/guzzle/docs/webservice-client/using-the-service-builder.rst @@ -0,0 +1,316 @@ +======================= +Using a service builder +======================= + +The best way to instantiate Guzzle web service clients is to let Guzzle handle building the clients for you using a +ServiceBuilder. A ServiceBuilder is responsible for creating concrete client objects based on configuration settings +and helps to manage credentials for different environments. + +You don't have to use a service builder, but they help to decouple your application from concrete classes and help to +share configuration data across multiple clients. Consider the following example. Here we are creating two clients that +require the same API public key and secret key. The clients are created using their ``factory()`` methods. + +.. code-block:: php + + use MyService\FooClient; + use MyService\BarClient; + + $foo = FooClient::factory(array( + 'key' => 'abc', + 'secret' => '123', + 'custom' => 'and above all' + )); + + $bar = BarClient::factory(array( + 'key' => 'abc', + 'secret' => '123', + 'custom' => 'listen to me' + )); + +The redundant specification of the API keys can be removed using a service builder. + +.. code-block:: php + + use Guzzle\Service\Builder\ServiceBuilder; + + $builder = ServiceBuilder::factory(array( + 'services' => array( + 'abstract_client' => array( + 'params' => array( + 'key' => 'abc', + 'secret' => '123' + ) + ), + 'foo' => array( + 'extends' => 'abstract_client', + 'class' => 'MyService\FooClient', + 'params' => array( + 'custom' => 'and above all' + ) + ), + 'bar' => array( + 'extends' => 'abstract_client', + 'class' => 'MyService\FooClient', + 'params' => array( + 'custom' => 'listen to me' + ) + ) + ) + )); + + $foo = $builder->get('foo'); + $bar = $builder->get('bar'); + +You can make managing your API keys even easier by saving the service builder configuration in a JSON format in a +.json file. + +Creating a service builder +-------------------------- + +A ServiceBuilder can source information from an array, an PHP include file that returns an array, or a JSON file. + +.. code-block:: php + + use Guzzle\Service\Builder\ServiceBuilder; + + // Source service definitions from a JSON file + $builder = ServiceBuilder::factory('services.json'); + +Sourcing data from an array +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Data can be source from a PHP array. The array must contain an associative ``services`` array that maps the name of a +client to the configuration information used by the service builder to create the client. Clients are given names +which are used to identify how a client is retrieved from a service builder. This can be useful for using multiple +accounts for the same service or creating development clients vs. production clients. + +.. code-block:: php + + $services = array( + 'includes' => array( + '/path/to/other/services.json', + '/path/to/other/php_services.php' + ), + 'services' => array( + 'abstract.foo' => array( + 'params' => array( + 'username' => 'foo', + 'password' => 'bar' + ) + ), + 'bar' => array( + 'extends' => 'abstract.foo', + 'class' => 'MyClientClass', + 'params' => array( + 'other' => 'abc' + ) + ) + ) + ); + +A service builder configuration array contains two top-level array keys: + ++------------+---------------------------------------------------------------------------------------------------------+ +| Key | Description | ++============+=========================================================================================================+ +| includes | Array of paths to JSON or PHP include files to include in the configuration. | ++------------+---------------------------------------------------------------------------------------------------------+ +| services | Associative array of defined services that can be created by the service builder. Each service can | +| | contain the following keys: | +| | | +| | +------------+----------------------------------------------------------------------------------------+ | +| | | Key | Description | | +| | +============+========================================================================================+ | +| | | class | The concrete class to instantiate that implements the | | +| | | | ``Guzzle\Common\FromConfigInterface``. | | +| | +------------+----------------------------------------------------------------------------------------+ | +| | | extends | The name of a previously defined service to extend from | | +| | +------------+----------------------------------------------------------------------------------------+ | +| | | params | Associative array of parameters to pass to the factory method of the service it is | | +| | | | instantiated | | +| | +------------+----------------------------------------------------------------------------------------+ | +| | | alias | An alias that can be used in addition to the array key for retrieving a client from | | +| | | | the service builder. | | +| | +------------+----------------------------------------------------------------------------------------+ | ++------------+---------------------------------------------------------------------------------------------------------+ + +The first client defined, ``abstract.foo``, is used as a placeholder of shared configuration values. Any service +extending abstract.foo will inherit its params. As an example, this can be useful when clients share the same username +and password. + +The next client, ``bar``, extends from ``abstract.foo`` using the ``extends`` attribute referencing the client from +which to extend. Additional parameters can be merged into the original service definition when extending a parent +service. + +.. important:: + + Each client that you intend to instantiate must specify a ``class`` attribute that references the full class name + of the client being created. The class referenced in the ``class`` parameter must implement a static ``factory()`` + method that accepts an array or ``Guzzle\Common\Collection`` object and returns an instantiated object. + +Sourcing from a PHP include +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can create service builder configurations using a PHP include file. This can be useful if you wish to take +advantage of an opcode cache like APC to speed up the process of loading and processing the configuration. The PHP +include file is the same format as an array, but you simply create a PHP script that returns an array and save the +file with the .php file extension. + +.. code-block:: php + + '...'); + // Saved as config.php + +This configuration file can then be used with a service builder. + +.. code-block:: php + + $builder = ServiceBuilder::factory('/path/to/config.php'); + +Sourcing from a JSON document +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can use JSON documents to serialize your service descriptions. The JSON format uses the exact same structure as +the PHP array syntax, but it's just serialized using JSON. + +.. code-block:: javascript + + { + "includes": ["/path/to/other/services.json", "/path/to/other/php_services.php"], + "services": { + "abstract.foo": { + "params": { + "username": "foo", + "password": "bar" + } + }, + "bar": { + "extends": "abstract.foo", + "class": "MyClientClass", + "params": { + "other": "abc" + } + } + } + } + +Referencing other clients in parameters +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If one of your clients depends on another client as one of its parameters, you can reference that client by name by +enclosing the client's reference key in ``{}``. + +.. code-block:: javascript + + { + "services": { + "token": { + "class": "My\Token\TokenFactory", + "params": { + "access_key": "xyz" + } + }, + "client": { + "class": "My\Client", + "params": { + "token_client": "{token}", + "version": "1.0" + } + } + } + } + +When ``client`` is constructed by the service builder, the service builder will first create the ``token`` service +and then inject the token service into ``client``'s factory method in the ``token_client`` parameter. + +Retrieving clients from a service builder +----------------------------------------- + +Clients are referenced using a customizable name you provide in your service definition. The ServiceBuilder is a sort +of multiton object-- it will only instantiate a client once and return that client for subsequent retrievals. Clients +are retrieved by name (the array key used in the configuration) or by the ``alias`` setting of a service. + +Here's an example of retrieving a client from your ServiceBuilder: + +.. code-block:: php + + $client = $builder->get('foo'); + + // You can also use the ServiceBuilder object as an array + $client = $builder['foo']; + +Creating throwaway clients +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can get a "throwaway" client (a client that is not persisted by the ServiceBuilder) by passing ``true`` in the +second argument of ``ServiceBuilder::get()``. This allows you to create a client that will not be returned by other +parts of your code that use the service builder. Instead of passing ``true``, you can pass an array of configuration +settings that will override the configuration settings specified in the service builder. + +.. code-block:: php + + // Get a throwaway client and overwrite the "custom" setting of the client + $foo = $builder->get('foo', array( + 'custom' => 'in this world there are rules' + )); + +Getting raw configuration settings +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can get the raw configuration settings provided to the service builder for a specific service using the +``getData($name)`` method of a service builder. This method will null if the service was not found in the service +builder or an array of configuration settings if the service was found. + +.. code-block:: php + + $data = $builder->getData('foo'); + echo $data['key'] . "\n"; + echo $data['secret'] . "\n"; + echo $data['custom'] . "\n"; + +Adding a plugin to all clients +------------------------------ + +You can add a plugin to all clients created by a service builder using the ``addGlobalPlugin($plugin)`` method of a +service builder and passing a ``Symfony\Component\EventDispatcher\EventSubscriberInterface`` object. The service builder +will then attach each global plugin to every client as it is created. This allows you to, for example, add a LogPlugin +to every request created by a service builder for easy debugging. + +.. code-block:: php + + use Guzzle\Plugin\Log\LogPlugin; + + // Add a debug log plugin to every client as it is created + $builder->addGlobalPlugin(LogPlugin::getDebugPlugin()); + + $foo = $builder->get('foo'); + $foo->get('/')->send(); + // Should output all of the data sent over the wire + +.. _service-builder-events: + +Events emitted from a service builder +------------------------------------- + +A ``Guzzle\Service\Builder\ServiceBuilder`` object emits the following events: + ++-------------------------------+--------------------------------------------+-----------------------------------------+ +| Event name | Description | Event data | ++===============================+============================================+=========================================+ +| service_builder.create_client | Called when a client is created | * client: The created client object | ++-------------------------------+--------------------------------------------+-----------------------------------------+ + +.. code-block:: php + + use Guzzle\Common\Event; + use Guzzle\Service\Builder\ServiceBuilder; + + $builder = ServiceBuilder::factory('/path/to/config.json'); + + // Add an event listener to print out each client client as it is created + $builder->getEventDispatcher()->addListener('service_builder.create_client', function (Event $e) { + echo 'Client created: ' . get_class($e['client']) . "\n"; + }); + + $foo = $builder->get('foo'); + // Should output the class used for the "foo" client diff --git a/guzzle/guzzle/docs/webservice-client/webservice-client.rst b/guzzle/guzzle/docs/webservice-client/webservice-client.rst new file mode 100644 index 000000000..7ec771e1c --- /dev/null +++ b/guzzle/guzzle/docs/webservice-client/webservice-client.rst @@ -0,0 +1,659 @@ +====================== +The web service client +====================== + +The ``Guzzle\Service`` namespace contains various abstractions that help to make it easier to interact with a web +service API, including commands, service descriptions, and resource iterators. + +In this chapter, we'll build a simple `Twitter API client `_. + +Creating a client +================= + +A class that extends from ``Guzzle\Service\Client`` or implements ``Guzzle\Service\ClientInterface`` must implement a +``factory()`` method in order to be used with a :doc:`service builder `. + +Factory method +-------------- + +You can use the ``factory()`` method of a client directly if you do not need a service builder. + +.. code-block:: php + + use mtdowling\TwitterClient; + + // Create a client and pass an array of configuration data + $twitter = TwitterClient::factory(array( + 'consumer_key' => '****', + 'consumer_secret' => '****', + 'token' => '****', + 'token_secret' => '****' + )); + +.. note:: + + If you'd like to follow along, here's how to get your Twitter API credentials: + + 1. Visit https://dev.twitter.com/apps + 2. Click on an application that you've created + 3. Click on the "OAuth tool" tab + 4. Copy all of the settings under "OAuth Settings" + +Implementing a factory method +----------------------------- + +Creating a client and its factory method is pretty simple. You just need to implement ``Guzzle\Service\ClientInterface`` +or extend from ``Guzzle\Service\Client``. + +.. code-block:: php + + namespace mtdowling; + + use Guzzle\Common\Collection; + use Guzzle\Plugin\Oauth\OauthPlugin; + use Guzzle\Service\Client; + use Guzzle\Service\Description\ServiceDescription; + + /** + * A simple Twitter API client + */ + class TwitterClient extends Client + { + public static function factory($config = array()) + { + // Provide a hash of default client configuration options + $default = array('base_url' => 'https://api.twitter.com/1.1'); + + // The following values are required when creating the client + $required = array( + 'base_url', + 'consumer_key', + 'consumer_secret', + 'token', + 'token_secret' + ); + + // Merge in default settings and validate the config + $config = Collection::fromConfig($config, $default, $required); + + // Create a new Twitter client + $client = new self($config->get('base_url'), $config); + + // Ensure that the OauthPlugin is attached to the client + $client->addSubscriber(new OauthPlugin($config->toArray())); + + return $client; + } + } + +Service Builder +--------------- + +A service builder is used to easily create web service clients, provides a simple configuration driven approach to +creating clients, and allows you to share configuration settings across multiple clients. You can find out more about +Guzzle's service builder in :doc:`using-the-service-builder`. + +.. code-block:: php + + use Guzzle\Service\Builder\ServiceBuilder; + + // Create a service builder and provide client configuration data + $builder = ServiceBuilder::factory('/path/to/client_config.json'); + + // Get the client from the service builder by name + $twitter = $builder->get('twitter'); + +The above example assumes you have JSON data similar to the following stored in "/path/to/client_config.json": + +.. code-block:: json + + { + "services": { + "twitter": { + "class": "mtdowling\\TwitterClient", + "params": { + "consumer_key": "****", + "consumer_secret": "****", + "token": "****", + "token_secret": "****" + } + } + } + } + +.. note:: + + A service builder becomes much more valuable when using multiple web service clients in a single application or + if you need to utilize the same client with varying configuration settings (e.g. multiple accounts). + +Commands +======== + +Commands are a concept in Guzzle that helps to hide the underlying implementation of an API by providing an easy to use +parameter driven object for each action of an API. A command is responsible for accepting an array of configuration +parameters, serializing an HTTP request, and parsing an HTTP response. Following the +`command pattern `_, commands in Guzzle offer a greater level of +flexibility when implementing and utilizing a web service client. + +Executing commands +------------------ + +You must explicitly execute a command after creating a command using the ``getCommand()`` method. A command has an +``execute()`` method that may be called, or you can use the ``execute()`` method of a client object and pass in the +command object. Calling either of these execute methods will return the result value of the command. The result value is +the result of parsing the HTTP response with the ``process()`` method. + +.. code-block:: php + + // Get a command from the client and pass an array of parameters + $command = $twitter->getCommand('getMentions', array( + 'count' => 5 + )); + + // Other parameters can be set on the command after it is created + $command['trim_user'] = false; + + // Execute the command using the command object. + // The result value contains an array of JSON data from the response + $result = $command->execute(); + + // You can retrieve the result of the command later too + $result = $command->getResult(). + +Command object also contains methods that allow you to inspect the HTTP request and response that was utilized with +the command. + +.. code-block:: php + + $request = $command->getRequest(); + $response = $command->getResponse(); + +.. note:: + + The format and notation used to retrieve commands from a client can be customized by injecting a custom command + factory, ``Guzzle\Service\Command\Factory\FactoryInterface``, on the client using ``$client->setCommandFactory()``. + +Executing with magic methods +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When using method missing magic methods with a command, the command will be executed right away and the result of the +command is returned. + +.. code-block:: php + + $jsonData = $twitter->getMentions(array( + 'count' => 5, + 'trim_user' => true + )); + +Creating commands +----------------- + +Commands are created using either the ``getCommand()`` method of a client or a magic missing method of a client. Using +the ``getCommand()`` method allows you to create a command without executing it, allowing for customization of the +command or the request serialized by the command. + +When a client attempts to create a command, it uses the client's ``Guzzle\Service\Command\Factory\FactoryInterface``. +By default, Guzzle will utilize a command factory that first looks for a concrete class for a particular command +(concrete commands) followed by a command defined by a service description (operation commands). We'll learn more about +concrete commands and operation commands later in this chapter. + +.. code-block:: php + + // Get a command from the twitter client. + $command = $twitter->getCommand('getMentions'); + $result = $command->execute(); + +Unless you've skipped ahead, running the above code will throw an exception. + + PHP Fatal error: Uncaught exception 'Guzzle\Common\Exception\InvalidArgumentException' with message + 'Command was not found matching getMentions' + +This exception was thrown because the "getMentions" command has not yet been implemented. Let's implement one now. + +Concrete commands +~~~~~~~~~~~~~~~~~ + +Commands can be created in one of two ways: create a concrete command class that extends +``Guzzle\Service\Command\AbstractCommand`` or +:doc:`create an OperationCommand based on a service description `. The recommended +approach is to use a service description to define your web service, but you can use concrete commands when custom +logic must be implemented for marshaling or unmarshaling a HTTP message. + +Commands are the method in which you abstract away the underlying format of the requests that need to be sent to take +action on a web service. Commands in Guzzle are meant to be built by executing a series of setter methods on a command +object. Commands are only validated right before they are executed. A ``Guzzle\Service\Client`` object is responsible +for executing commands. Commands created for your web service must implement +``Guzzle\Service\Command\CommandInterface``, but it's easier to extend the ``Guzzle\Service\Command\AbstractCommand`` +class, implement the ``build()`` method, and optionally implement the ``process()`` method. + +Serializing requests +^^^^^^^^^^^^^^^^^^^^ + +The ``build()`` method of a command is responsible for using the arguments of the command to build and serialize a +HTTP request and set the request on the ``$request`` property of the command object. This step is usually taken care of +for you when using a service description driven command that uses the default +``Guzzle\Service\Command\OperationCommand``. You may wish to implement the process method yourself when you aren't +using a service description or need to implement more complex request serialization. + +.. important:::: + + When implementing a custom ``build()`` method, be sure to set the class property of ``$this->request`` to an + instantiated and ready to send request. + +The following example shows how to implement the ``getMentions`` +`Twitter API `_ method using a concrete command. + +.. code-block:: php + + namespace mtdowling\Twitter\Command; + + use Guzzle\Service\Command\AbstractCommand; + + class GetMentions extends AbstractCommand + { + protected function build() + { + // Create the request property of the command + $this->request = $this->client->get('statuses/mentions_timeline.json'); + + // Grab the query object of the request because we will use it for + // serializing command parameters on the request + $query = $this->request->getQuery(); + + if ($this['count']) { + $query->set('count', $this['count']); + } + + if ($this['since_id']) { + $query->set('since_id', $this['since_id']); + } + + if ($this['max_id']) { + $query->set('max_id', $this['max_id']); + } + + if ($this['trim_user'] !== null) { + $query->set('trim_user', $this['trim_user'] ? 'true' : 'false'); + } + + if ($this['contributor_details'] !== null) { + $query->set('contributor_details', $this['contributor_details'] ? 'true' : 'false'); + } + + if ($this['include_entities'] !== null) { + $query->set('include_entities', $this['include_entities'] ? 'true' : 'false'); + } + } + } + +By default, a client will attempt to find concrete command classes under the ``Command`` namespace of a client. First +the client will attempt to find an exact match for the name of the command to the name of the command class. If an +exact match is not found, the client will calculate a class name using inflection. This is calculated based on the +folder hierarchy of a command and converting the CamelCased named commands into snake_case. Here are some examples on +how the command names are calculated: + +#. ``Foo\Command\JarJar`` **->** jar_jar +#. ``Foo\Command\Test`` **->** test +#. ``Foo\Command\People\GetCurrentPerson`` **->** people.get_current_person + +Notice how any sub-namespace beneath ``Command`` is converted from ``\`` to ``.`` (a period). CamelCasing is converted +to lowercased snake_casing (e.g. JarJar == jar_jar). + +Parsing responses +^^^^^^^^^^^^^^^^^ + +The ``process()`` method of a command is responsible for converting an HTTP response into something more useful. For +example, a service description operation that has specified a model object in the ``responseClass`` attribute of the +operation will set a ``Guzzle\Service\Resource\Model`` object as the result of the command. This behavior can be +completely modified as needed-- even if you are using operations and responseClass models. Simply implement a custom +``process()`` method that sets the ``$this->result`` class property to whatever you choose. You can reuse parts of the +default Guzzle response parsing functionality or get inspiration from existing code by using +``Guzzle\Service\Command\OperationResponseParser`` and ``Guzzle\Service\Command\DefaultResponseParser`` classes. + +If you do not implement a custom ``process()`` method and are not using a service description, then Guzzle will attempt +to guess how a response should be processed based on the Content-Type header of the response. Because the Twitter API +sets a ``Content-Type: application/json`` header on this response, we do not need to implement any custom response +parsing. + +Operation commands +~~~~~~~~~~~~~~~~~~ + +Operation commands are commands in which the serialization of an HTTP request and the parsing of an HTTP response are +driven by a Guzzle service description. Because request serialization, validation, and response parsing are +described using a DSL, creating operation commands is a much faster process than writing concrete commands. + +Creating operation commands for our Twitter client can remove a great deal of redundancy from the previous concrete +command, and allows for a deeper runtime introspection of the API. Here's an example service description we can use to +create the Twitter API client: + +.. code-block:: json + + { + "name": "Twitter", + "apiVersion": "1.1", + "baseUrl": "https://api.twitter.com/1.1", + "description": "Twitter REST API client", + "operations": { + "GetMentions": { + "httpMethod": "GET", + "uri": "statuses/mentions_timeline.json", + "summary": "Returns the 20 most recent mentions for the authenticating user.", + "responseClass": "GetMentionsOutput", + "parameters": { + "count": { + "description": "Specifies the number of tweets to try and retrieve", + "type": "integer", + "location": "query" + }, + "since_id": { + "description": "Returns results with an ID greater than the specified ID", + "type": "integer", + "location": "query" + }, + "max_id": { + "description": "Returns results with an ID less than or equal to the specified ID.", + "type": "integer", + "location": "query" + }, + "trim_user": { + "description": "Limits the amount of data returned for each user", + "type": "boolean", + "location": "query" + }, + "contributor_details": { + "description": "Adds more data to contributor elements", + "type": "boolean", + "location": "query" + }, + "include_entities": { + "description": "The entities node will be disincluded when set to false.", + "type": "boolean", + "location": "query" + } + } + } + }, + "models": { + "GetMentionsOutput": { + "type": "object", + "additionalProperties": { + "location": "json" + } + } + } + } + +If you're lazy, you can define the API in a less descriptive manner using ``additionalParameters``. +``additionalParameters`` define the serialization and validation rules of parameters that are not explicitly defined +in a service description. + +.. code-block:: json + + { + "name": "Twitter", + "apiVersion": "1.1", + "baseUrl": "https://api.twitter.com/1.1", + "description": "Twitter REST API client", + "operations": { + "GetMentions": { + "httpMethod": "GET", + "uri": "statuses/mentions_timeline.json", + "summary": "Returns the 20 most recent mentions for the authenticating user.", + "responseClass": "GetMentionsOutput", + "additionalParameters": { + "location": "query" + } + } + }, + "models": { + "GetMentionsOutput": { + "type": "object", + "additionalProperties": { + "location": "json" + } + } + } + } + +You should attach the service description to the client at the end of the client's factory method: + +.. code-block:: php + + // ... + class TwitterClient extends Client + { + public static function factory($config = array()) + { + // ... same code as before ... + + // Set the service description + $client->setDescription(ServiceDescription::factory('path/to/twitter.json')); + + return $client; + } + } + +The client can now use operations defined in the service description instead of requiring you to create concrete +command classes. Feel free to delete the concrete command class we created earlier. + +.. code-block:: php + + $jsonData = $twitter->getMentions(array( + 'count' => 5, + 'trim_user' => true + )); + +Executing commands in parallel +------------------------------ + +Much like HTTP requests, Guzzle allows you to send multiple commands in parallel. You can send commands in parallel by +passing an array of command objects to a client's ``execute()`` method. The client will serialize each request and +send them all in parallel. If an error is encountered during the transfer, then a +``Guzzle\Service\Exception\CommandTransferException`` is thrown, which allows you to retrieve a list of commands that +succeeded and a list of commands that failed. + +.. code-block:: php + + use Guzzle\Service\Exception\CommandTransferException; + + $commands = array(); + $commands[] = $twitter->getCommand('getMentions'); + $commands[] = $twitter->getCommand('otherCommandName'); + // etc... + + try { + $result = $client->execute($commands); + foreach ($result as $command) { + echo $command->getName() . ': ' . $command->getResponse()->getStatusCode() . "\n"; + } + } catch (CommandTransferException $e) { + // Get an array of the commands that succeeded + foreach ($e->getSuccessfulCommands() as $command) { + echo $command->getName() . " succeeded\n"; + } + // Get an array of the commands that failed + foreach ($e->getFailedCommands() as $command) { + echo $command->getName() . " failed\n"; + } + } + +.. note:: + + All commands executed from a client using an array must originate from the same client. + +Special command options +----------------------- + +Guzzle exposes several options that help to control how commands are validated, serialized, and parsed. +Command options can be specified when creating a command or in the ``command.params`` parameter in the +``Guzzle\Service\Client``. + +=========================== ============================================================================================ +command.request_options Option used to add :ref:`Request options ` to the request created by a + command +command.hidden_params An array of the names of parameters ignored by the ``additionalParameters`` parameter schema +command.disable_validation Set to true to disable JSON schema validation of the command's input parameters +command.response_processing Determines how the default response parser will parse the command. One of "raw" no parsing, + "model" (the default method used to parse commands using response models defined in service + descriptions) +command.headers (deprecated) Option used to specify custom headers. Use ``command.request_options`` instead +command.on_complete (deprecated) Option used to add an onComplete method to a command. Use + ``command.after_send`` event instead +command.response_body (deprecated) Option used to change the entity body used to store a response. + Use ``command.request_options`` instead +=========================== ============================================================================================ + +Advanced client configuration +============================= + +Default command parameters +-------------------------- + +When creating a client object, you can specify default command parameters to pass into all commands. Any key value pair +present in the ``command.params`` settings of a client will be added as default parameters to any command created +by the client. + +.. code-block:: php + + $client = new Guzzle\Service\Client(array( + 'command.params' => array( + 'default_1' => 'foo', + 'another' => 'bar' + ) + )); + +Magic methods +------------- + +Client objects will, by default, attempt to create and execute commands when a missing method is invoked on a client. +This powerful concept applies to both concrete commands and operation commands powered by a service description. This +makes it appear to the end user that you have defined actual methods on a client object, when in fact, the methods are +invoked using PHP's magic ``__call`` method. + +The ``__call`` method uses the ``getCommand()`` method of a client, which uses the client's internal +``Guzzle\Service\Command\Factory\FactoryInterface`` object. The default command factory allows you to instantiate +operations defined in a client's service description. The method in which a client determines which command to +execute is defined as follows: + +1. The client will first try to find a literal match for an operation in the service description. +2. If the literal match is not found, the client will try to uppercase the first character of the operation and find + the match again. +3. If a match is still not found, the command factory will inflect the method name from CamelCase to snake_case and + attempt to find a matching command. +4. If a command still does not match, an exception is thrown. + +.. code-block:: php + + // Use the magic method + $result = $twitter->getMentions(); + + // This is exactly the same as: + $result = $twitter->getCommand('getMentions')->execute(); + +You can disable magic methods on a client by passing ``false`` to the ``enableMagicMethod()`` method. + +Custom command factory +---------------------- + +A client by default uses the ``Guzzle\Service\Command\Factory\CompositeFactory`` which allows multiple command +factories to attempt to create a command by a certain name. The default CompositeFactory uses a ``ConcreteClassFactory`` +and a ``ServiceDescriptionFactory`` if a service description is specified on a client. You can specify a custom +command factory if your client requires custom command creation logic using the ``setCommandFactory()`` method of +a client. + +Custom resource Iterator factory +-------------------------------- + +Resource iterators can be retrieved from a client using the ``getIterator($name)`` method of a client. This method uses +a client's internal ``Guzzle\Service\Resource\ResourceIteratorFactoryInterface`` object. A client by default uses a +``Guzzle\Service\Resource\ResourceIteratorClassFactory`` to attempt to find concrete classes that implement resource +iterators. The default factory will first look for matching iterators in the ``Iterator`` subdirectory of the client +followed by the ``Model`` subdirectory of a client. Use the ``setResourceIteratorFactory()`` method of a client to +specify a custom resource iterator factory. + +Plugins and events +================== + +``Guzzle\Service\Client`` exposes various events that allow you to hook in custom logic. A client object owns a +``Symfony\Component\EventDispatcher\EventDispatcher`` object that can be accessed by calling +``$client->getEventDispatcher()``. You can use the event dispatcher to add listeners (a simple callback function) or +event subscribers (classes that listen to specific events of a dispatcher). + +.. _service-client-events: + +Events emitted from a Service Client +------------------------------------ + +A ``Guzzle\Service\Client`` object emits the following events: + ++------------------------------+--------------------------------------------+------------------------------------------+ +| Event name | Description | Event data | ++==============================+============================================+==========================================+ +| client.command.create | The client created a command object | * client: Client object | +| | | * command: Command object | ++------------------------------+--------------------------------------------+------------------------------------------+ +| command.before_prepare | Before a command is validated and built. | * command: Command being prepared | +| | This is also before a request is created. | | ++------------------------------+--------------------------------------------+------------------------------------------+ +| command.after_prepare | After a command instantiates and | * command: Command that was prepared | +| | configures its request object. | | ++------------------------------+--------------------------------------------+------------------------------------------+ +| command.before_send | The client is about to execute a prepared | * command: Command to execute | +| | command | | ++------------------------------+--------------------------------------------+------------------------------------------+ +| command.after_send | The client successfully completed | * command: The command that was executed | +| | executing a command | | ++------------------------------+--------------------------------------------+------------------------------------------+ +| command.parse_response | Called when ``responseType`` is ``class`` | * command: The command with a response | +| | and the response is about to be parsed. | about to be parsed. | ++------------------------------+--------------------------------------------+------------------------------------------+ + +.. code-block:: php + + use Guzzle\Common\Event; + use Guzzle\Service\Client; + + $client = new Client(); + + // create an event listener that operates on request objects + $client->getEventDispatcher()->addListener('command.after_prepare', function (Event $event) { + $command = $event['command']; + $request = $command->getRequest(); + + // do something with request + }); + +.. code-block:: php + + use Guzzle\Common\Event; + use Guzzle\Common\Client; + use Symfony\Component\EventDispatcher\EventSubscriberInterface; + + class EventSubscriber implements EventSubscriberInterface + { + public static function getSubscribedEvents() + { + return array( + 'client.command.create' => 'onCommandCreate', + 'command.parse_response' => 'onParseResponse' + ); + } + + public function onCommandCreate(Event $event) + { + $client = $event['client']; + $command = $event['command']; + // operate on client and command + } + + public function onParseResponse(Event $event) + { + $command = $event['command']; + // operate on the command + } + } + + $client = new Client(); + + $client->addSubscriber(new EventSubscriber()); diff --git a/guzzle/guzzle/phar-stub.php b/guzzle/guzzle/phar-stub.php new file mode 100644 index 000000000..cc2b53f4f --- /dev/null +++ b/guzzle/guzzle/phar-stub.php @@ -0,0 +1,16 @@ +registerNamespaces(array( + 'Guzzle' => 'phar://guzzle.phar/src', + 'Symfony\\Component\\EventDispatcher' => 'phar://guzzle.phar/vendor/symfony/event-dispatcher', + 'Doctrine' => 'phar://guzzle.phar/vendor/doctrine/common/lib', + 'Monolog' => 'phar://guzzle.phar/vendor/monolog/monolog/src' +)); +$classLoader->register(); + +__HALT_COMPILER(); diff --git a/guzzle/guzzle/phing/build.properties.dist b/guzzle/guzzle/phing/build.properties.dist new file mode 100644 index 000000000..c60d3d9cf --- /dev/null +++ b/guzzle/guzzle/phing/build.properties.dist @@ -0,0 +1,16 @@ +# you may need to update this if you're working on a fork. +guzzle.remote=git@github.com:guzzle/guzzle.git + +# github credentials -- only used by GitHub API calls to create subtree repos +github.basicauth=username:password +# for the subtree split and testing +github.org=guzzle + +# your git path +cmd.git=git + +# your composer command +cmd.composer=composer + +# test server start +cmd.testserver=node diff --git a/guzzle/guzzle/phing/imports/dependencies.xml b/guzzle/guzzle/phing/imports/dependencies.xml new file mode 100644 index 000000000..e40e037c2 --- /dev/null +++ b/guzzle/guzzle/phing/imports/dependencies.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + using git at ${cmd.git} + + + + found git at ${cmd.git} + + + + + + + + + + diff --git a/guzzle/guzzle/phing/imports/deploy.xml b/guzzle/guzzle/phing/imports/deploy.xml new file mode 100644 index 000000000..8476132e7 --- /dev/null +++ b/guzzle/guzzle/phing/imports/deploy.xml @@ -0,0 +1,142 @@ + + + + + + + + + + + On branch ${head} + + + + + + + + + + working directory clean + + + ${git.status} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ChangeLog Match: ${version.changelog} + Guzzle\Common\Version Match: ${version.version} + + + + releasing: phing -Dnew.version=3.0.x -Dhead=master release + -- + + + + + + + + + + + + + + + BEGINNING RELEASE FOR ${new.version} + + + + + + + + + + + + + + + + + + + + + + + + Tip: to create a new release, do: phing -Dnew.version=[TAG] -Dhead=[BRANCH] release + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/guzzle/guzzle/phing/tasks/ComposerLintTask.php b/guzzle/guzzle/phing/tasks/ComposerLintTask.php new file mode 100644 index 000000000..3b7040982 --- /dev/null +++ b/guzzle/guzzle/phing/tasks/ComposerLintTask.php @@ -0,0 +1,152 @@ + + * @license http://claylo.mit-license.org/2012/ MIT License + */ + +require_once 'phing/Task.php'; + +class ComposerLintTask extends Task +{ + protected $dir = null; + protected $file = null; + protected $passthru = false; + protected $composer = null; + + /** + * The setter for the dir + * + * @param string $str Directory to crawl recursively for composer files + */ + public function setDir($str) + { + $this->dir = $str; + } + + /** + * The setter for the file + * + * @param string $str Individual file to validate + */ + public function setFile($str) + { + $this->file = $str; + } + + /** + * Whether to use PHP's passthru() function instead of exec() + * + * @param boolean $passthru If passthru shall be used + */ + public function setPassthru($passthru) + { + $this->passthru = (bool) $passthru; + } + + /** + * Composer to execute. If unset, will attempt composer.phar in project + * basedir, and if that fails, will attempt global composer + * installation. + * + * @param string $str Individual file to validate + */ + public function setComposer($str) + { + $this->file = $str; + } + + /** + * The init method: do init steps + */ + public function init() + { + // nothing needed here + } + + /** + * The main entry point + */ + public function main() + { + if ($this->composer === null) { + $this->findComposer(); + } + + $files = array(); + if (!empty($this->file) && file_exists($this->file)) { + $files[] = $this->file; + } + + if (!empty($this->dir)) { + $found = $this->findFiles(); + foreach ($found as $file) { + $files[] = $this->dir . DIRECTORY_SEPARATOR . $file; + } + } + + foreach ($files as $file) { + + $cmd = $this->composer . ' validate ' . $file; + $cmd = escapeshellcmd($cmd); + + if ($this->passthru) { + $retval = null; + passthru($cmd, $retval); + if ($retval == 1) { + throw new BuildException('invalid composer.json'); + } + } else { + $out = array(); + $retval = null; + exec($cmd, $out, $retval); + if ($retval == 1) { + $err = join("\n", $out); + throw new BuildException($err); + } else { + $this->log($out[0]); + } + } + + } + + } + + /** + * Find the composer.json files using Phing's directory scanner + * + * @return array + */ + protected function findFiles() + { + $ds = new DirectoryScanner(); + $ds->setBasedir($this->dir); + $ds->setIncludes(array('**/composer.json')); + $ds->scan(); + return $ds->getIncludedFiles(); + } + + /** + * Find composer installation + * + */ + protected function findComposer() + { + $basedir = $this->project->getBasedir(); + $php = $this->project->getProperty('php.interpreter'); + + if (file_exists($basedir . '/composer.phar')) { + $this->composer = "$php $basedir/composer.phar"; + } else { + $out = array(); + exec('which composer', $out); + if (empty($out)) { + throw new BuildException( + 'Could not determine composer location.' + ); + } + $this->composer = $out[0]; + } + } +} diff --git a/guzzle/guzzle/phing/tasks/GuzzlePearPharPackageTask.php b/guzzle/guzzle/phing/tasks/GuzzlePearPharPackageTask.php new file mode 100644 index 000000000..f72a6b5d0 --- /dev/null +++ b/guzzle/guzzle/phing/tasks/GuzzlePearPharPackageTask.php @@ -0,0 +1,338 @@ + + * @license http://claylo.mit-license.org/2012/ MIT License + */ + +require_once 'phing/Task.php'; +require_once 'PEAR/PackageFileManager2.php'; +require_once 'PEAR/PackageFileManager/File.php'; +require_once 'PEAR/Packager.php'; + +class GuzzlePearPharPackageTask extends Task +{ + private $version; + private $deploy = true; + private $makephar = true; + + private $subpackages = array(); + + public function setVersion($str) + { + $this->version = $str; + } + + public function getVersion() + { + return $this->version; + } + + public function setDeploy($deploy) + { + $this->deploy = (bool) $deploy; + } + + public function getDeploy() + { + return $this->deploy; + } + + public function setMakephar($makephar) + { + $this->makephar = (bool) $makephar; + } + + public function getMakephar() + { + return $this->makephar; + } + + private $basedir; + private $guzzleinfo; + private $changelog_release_date; + private $changelog_notes = '-'; + + public function main() + { + $this->basedir = $this->getProject()->getBasedir(); + + if (!is_dir((string) $this->basedir.'/.subsplit')) { + throw new BuildException('PEAR packaging requires .subsplit directory'); + } + + // main composer file + $composer_file = file_get_contents((string) $this->basedir.'/.subsplit/composer.json'); + $this->guzzleinfo = json_decode($composer_file, true); + + // make sure we have a target + $pearwork = (string) $this->basedir . '/build/pearwork'; + if (!is_dir($pearwork)) { + mkdir($pearwork, 0777, true); + } + $pearlogs = (string) $this->basedir . '/build/artifacts/logs'; + if (!is_dir($pearlogs)) { + mkdir($pearlogs, 0777, true); + } + + $version = $this->getVersion(); + $this->grabChangelog(); + if ($version[0] == '2') { + $this->log('building single PEAR package'); + $this->buildSinglePackage(); + } else { + // $this->log("building PEAR subpackages"); + // $this->createSubPackages(); + // $this->log("building PEAR bundle package"); + $this->buildSinglePackage(); + } + + if ($this->getMakephar()) { + $this->log("building PHAR"); + $this->getProject()->executeTarget('package-phar'); + } + + if ($this->getDeploy()) { + $this->doDeployment(); + } + } + + public function doDeployment() + { + $basedir = (string) $this->basedir; + $this->log('beginning PEAR/PHAR deployment'); + + chdir($basedir . '/build/pearwork'); + if (!is_dir('./channel')) { + mkdir('./channel'); + } + + // Pull the PEAR channel down locally + passthru('aws s3 sync s3://pear.guzzlephp.org ./channel'); + + // add PEAR packages + foreach (scandir('./') as $file) { + if (substr($file, -4) == '.tgz') { + passthru('pirum add ./channel ' . $file); + } + } + + // if we have a new phar, add it + if ($this->getMakephar() && file_exists($basedir . '/build/artifacts/guzzle.phar')) { + rename($basedir . '/build/artifacts/guzzle.phar', './channel/guzzle.phar'); + } + + // Sync up with the S3 bucket + chdir($basedir . '/build/pearwork/channel'); + passthru('aws s3 sync . s3://pear.guzzlephp.org'); + } + + public function buildSinglePackage() + { + $v = $this->getVersion(); + $apiversion = $v[0] . '.0.0'; + + $opts = array( + 'packagedirectory' => (string) $this->basedir . '/.subsplit/src/', + 'filelistgenerator' => 'file', + 'ignore' => array('*composer.json'), + 'baseinstalldir' => '/', + 'packagefile' => 'package.xml' + //'outputdirectory' => (string) $this->basedir . '/build/pearwork/' + ); + $pfm = new PEAR_PackageFileManager2(); + $pfm->setOptions($opts); + $pfm->addRole('md', 'doc'); + $pfm->addRole('pem', 'php'); + $pfm->setPackage('Guzzle'); + $pfm->setSummary("Object-oriented PHP HTTP Client for PHP 5.3+"); + $pfm->setDescription($this->guzzleinfo['description']); + $pfm->setPackageType('php'); + $pfm->setChannel('guzzlephp.org/pear'); + $pfm->setAPIVersion($apiversion); + $pfm->setReleaseVersion($this->getVersion()); + $pfm->setAPIStability('stable'); + $pfm->setReleaseStability('stable'); + $pfm->setNotes($this->changelog_notes); + $pfm->setPackageType('php'); + $pfm->setLicense('MIT', 'https://github.com/guzzle/guzzle/blob/master/LICENSE'); + $pfm->addMaintainer('lead', 'mtdowling', 'Michael Dowling', 'mtdowling@gmail.com', 'yes'); + $pfm->setDate($this->changelog_release_date); + $pfm->generateContents(); + + $phpdep = $this->guzzleinfo['require']['php']; + $phpdep = str_replace('>=', '', $phpdep); + $pfm->setPhpDep($phpdep); + $pfm->addExtensionDep('required', 'curl'); + $pfm->setPearinstallerDep('1.4.6'); + $pfm->addPackageDepWithChannel('required', 'EventDispatcher', 'pear.symfony.com', '2.1.0'); + if (!empty($this->subpackages)) { + foreach ($this->subpackages as $package) { + $pkg = dirname($package); + $pkg = str_replace('/', '_', $pkg); + $pfm->addConflictingPackageDepWithChannel($pkg, 'guzzlephp.org/pear', false, $apiversion); + } + } + + ob_start(); + $startdir = getcwd(); + chdir((string) $this->basedir . '/build/pearwork'); + + echo "DEBUGGING GENERATED PACKAGE FILE\n"; + $result = $pfm->debugPackageFile(); + if ($result) { + $out = $pfm->writePackageFile(); + echo "\n\n\nWRITE PACKAGE FILE RESULT:\n"; + var_dump($out); + // load up package file and build package + $packager = new PEAR_Packager(); + echo "\n\n\nBUILDING PACKAGE FROM PACKAGE FILE:\n"; + $dest_package = $packager->package($opts['packagedirectory'].'package.xml'); + var_dump($dest_package); + } else { + echo "\n\n\nDEBUGGING RESULT:\n"; + var_dump($result); + } + echo "removing package.xml"; + unlink($opts['packagedirectory'].'package.xml'); + $log = ob_get_clean(); + file_put_contents((string) $this->basedir . '/build/artifacts/logs/pear_package.log', $log); + chdir($startdir); + } + + public function createSubPackages() + { + $this->findComponents(); + + foreach ($this->subpackages as $package) { + $baseinstalldir = dirname($package); + $dir = (string) $this->basedir.'/.subsplit/src/' . $baseinstalldir; + $composer_file = file_get_contents((string) $this->basedir.'/.subsplit/src/'. $package); + $package_info = json_decode($composer_file, true); + $this->log('building ' . $package_info['target-dir'] . ' subpackage'); + $this->buildSubPackage($dir, $baseinstalldir, $package_info); + } + } + + public function buildSubPackage($dir, $baseinstalldir, $info) + { + $package = str_replace('/', '_', $baseinstalldir); + $opts = array( + 'packagedirectory' => $dir, + 'filelistgenerator' => 'file', + 'ignore' => array('*composer.json', '*package.xml'), + 'baseinstalldir' => '/' . $info['target-dir'], + 'packagefile' => 'package.xml' + ); + $pfm = new PEAR_PackageFileManager2(); + $pfm->setOptions($opts); + $pfm->setPackage($package); + $pfm->setSummary($info['description']); + $pfm->setDescription($info['description']); + $pfm->setPackageType('php'); + $pfm->setChannel('guzzlephp.org/pear'); + $pfm->setAPIVersion('3.0.0'); + $pfm->setReleaseVersion($this->getVersion()); + $pfm->setAPIStability('stable'); + $pfm->setReleaseStability('stable'); + $pfm->setNotes($this->changelog_notes); + $pfm->setPackageType('php'); + $pfm->setLicense('MIT', 'https://github.com/guzzle/guzzle/blob/master/LICENSE'); + $pfm->addMaintainer('lead', 'mtdowling', 'Michael Dowling', 'mtdowling@gmail.com', 'yes'); + $pfm->setDate($this->changelog_release_date); + $pfm->generateContents(); + + $phpdep = $this->guzzleinfo['require']['php']; + $phpdep = str_replace('>=', '', $phpdep); + $pfm->setPhpDep($phpdep); + $pfm->setPearinstallerDep('1.4.6'); + + foreach ($info['require'] as $type => $version) { + if ($type == 'php') { + continue; + } + if ($type == 'symfony/event-dispatcher') { + $pfm->addPackageDepWithChannel('required', 'EventDispatcher', 'pear.symfony.com', '2.1.0'); + } + if ($type == 'ext-curl') { + $pfm->addExtensionDep('required', 'curl'); + } + if (substr($type, 0, 6) == 'guzzle') { + $gdep = str_replace('/', ' ', $type); + $gdep = ucwords($gdep); + $gdep = str_replace(' ', '_', $gdep); + $pfm->addPackageDepWithChannel('required', $gdep, 'guzzlephp.org/pear', $this->getVersion()); + } + } + + // can't have main Guzzle package AND sub-packages + $pfm->addConflictingPackageDepWithChannel('Guzzle', 'guzzlephp.org/pear', false, $apiversion); + + ob_start(); + $startdir = getcwd(); + chdir((string) $this->basedir . '/build/pearwork'); + + echo "DEBUGGING GENERATED PACKAGE FILE\n"; + $result = $pfm->debugPackageFile(); + if ($result) { + $out = $pfm->writePackageFile(); + echo "\n\n\nWRITE PACKAGE FILE RESULT:\n"; + var_dump($out); + // load up package file and build package + $packager = new PEAR_Packager(); + echo "\n\n\nBUILDING PACKAGE FROM PACKAGE FILE:\n"; + $dest_package = $packager->package($opts['packagedirectory'].'/package.xml'); + var_dump($dest_package); + } else { + echo "\n\n\nDEBUGGING RESULT:\n"; + var_dump($result); + } + echo "removing package.xml"; + unlink($opts['packagedirectory'].'/package.xml'); + $log = ob_get_clean(); + file_put_contents((string) $this->basedir . '/build/artifacts/logs/pear_package_'.$package.'.log', $log); + chdir($startdir); + } + + public function findComponents() + { + $ds = new DirectoryScanner(); + $ds->setBasedir((string) $this->basedir.'/.subsplit/src'); + $ds->setIncludes(array('**/composer.json')); + $ds->scan(); + $files = $ds->getIncludedFiles(); + $this->subpackages = $files; + } + + public function grabChangelog() + { + $cl = file((string) $this->basedir.'/.subsplit/CHANGELOG.md'); + $notes = ''; + $in_version = false; + $release_date = null; + + foreach ($cl as $line) { + $line = trim($line); + if (preg_match('/^\* '.$this->getVersion().' \(([0-9\-]+)\)$/', $line, $matches)) { + $release_date = $matches[1]; + $in_version = true; + continue; + } + if ($in_version && empty($line) && empty($notes)) { + continue; + } + if ($in_version && ! empty($line)) { + $notes .= $line."\n"; + } + if ($in_version && empty($line) && !empty($notes)) { + $in_version = false; + } + } + $this->changelog_release_date = $release_date; + + if (! empty($notes)) { + $this->changelog_notes = $notes; + } + } +} diff --git a/guzzle/guzzle/phing/tasks/GuzzleSubSplitTask.php b/guzzle/guzzle/phing/tasks/GuzzleSubSplitTask.php new file mode 100644 index 000000000..5d56a5bd5 --- /dev/null +++ b/guzzle/guzzle/phing/tasks/GuzzleSubSplitTask.php @@ -0,0 +1,385 @@ + + * @license http://claylo.mit-license.org/2012/ MIT License + */ + +require_once 'phing/tasks/ext/git/GitBaseTask.php'; + +// base - base of tree to split out +// subIndicatorFile - composer.json, package.xml? +class GuzzleSubSplitTask extends GitBaseTask +{ + /** + * What git repository to pull from and publish to + */ + protected $remote = null; + + /** + * Publish for comma-separated heads instead of all heads + */ + protected $heads = null; + + /** + * Publish for comma-separated tags instead of all tags + */ + protected $tags = null; + + /** + * Base of the tree RELATIVE TO .subsplit working dir + */ + protected $base = null; + + /** + * The presence of this file will indicate that the directory it resides + * in is at the top level of a split. + */ + protected $subIndicatorFile = 'composer.json'; + + /** + * Do everything except actually send the update. + */ + protected $dryRun = null; + + /** + * Do not sync any heads. + */ + protected $noHeads = false; + + /** + * Do not sync any tags. + */ + protected $noTags = false; + + /** + * The splits we found in the heads + */ + protected $splits; + + public function setRemote($str) + { + $this->remote = $str; + } + + public function getRemote() + { + return $this->remote; + } + + public function setHeads($str) + { + $this->heads = explode(',', $str); + } + + public function getHeads() + { + return $this->heads; + } + + public function setTags($str) + { + $this->tags = explode(',', $str); + } + + public function getTags() + { + return $this->tags; + } + + public function setBase($str) + { + $this->base = $str; + } + + public function getBase() + { + return $this->base; + } + + public function setSubIndicatorFile($str) + { + $this->subIndicatorFile = $str; + } + + public function getSubIndicatorFile() + { + return $this->subIndicatorFile; + } + + public function setDryRun($bool) + { + $this->dryRun = (bool) $bool; + } + + public function getDryRun() + { + return $this->dryRun; + } + + public function setNoHeads($bool) + { + $this->noHeads = (bool) $bool; + } + + public function getNoHeads() + { + return $this->noHeads; + } + + public function setNoTags($bool) + { + $this->noTags = (bool) $bool; + } + + public function getNoTags() + { + return $this->noTags; + } + + /** + * GitClient from VersionControl_Git + */ + protected $client = null; + + /** + * The main entry point + */ + public function main() + { + $repo = $this->getRepository(); + if (empty($repo)) { + throw new BuildException('"repository" is a required parameter'); + } + + $remote = $this->getRemote(); + if (empty($remote)) { + throw new BuildException('"remote" is a required parameter'); + } + + chdir($repo); + $this->client = $this->getGitClient(false, $repo); + + // initalized yet? + if (!is_dir('.subsplit')) { + $this->subsplitInit(); + } else { + // update + $this->subsplitUpdate(); + } + + // find all splits based on heads requested + $this->findSplits(); + + // check that GitHub has the repos + $this->verifyRepos(); + + // execute the subsplits + $this->publish(); + } + + public function publish() + { + $this->log('DRY RUN ONLY FOR NOW'); + $base = $this->getBase(); + $base = rtrim($base, '/') . '/'; + $org = $this->getOwningTarget()->getProject()->getProperty('github.org'); + + $splits = array(); + + $heads = $this->getHeads(); + foreach ($heads as $head) { + foreach ($this->splits[$head] as $component => $meta) { + $splits[] = $base . $component . ':git@github.com:'. $org.'/'.$meta['repo']; + } + + $cmd = 'git subsplit publish '; + $cmd .= escapeshellarg(implode(' ', $splits)); + + if ($this->getNoHeads()) { + $cmd .= ' --no-heads'; + } else { + $cmd .= ' --heads='.$head; + } + + if ($this->getNoTags()) { + $cmd .= ' --no-tags'; + } else { + if ($this->getTags()) { + $cmd .= ' --tags=' . escapeshellarg(implode(' ', $this->getTags())); + } + } + + passthru($cmd); + } + } + + /** + * Runs `git subsplit update` + */ + public function subsplitUpdate() + { + $repo = $this->getRepository(); + $this->log('git-subsplit update...'); + $cmd = $this->client->getCommand('subsplit'); + $cmd->addArgument('update'); + try { + $cmd->execute(); + } catch (Exception $e) { + throw new BuildException('git subsplit update failed'. $e); + } + chdir($repo . '/.subsplit'); + passthru('php ../composer.phar update --dev'); + chdir($repo); + } + + /** + * Runs `git subsplit init` based on the remote repository. + */ + public function subsplitInit() + { + $remote = $this->getRemote(); + $cmd = $this->client->getCommand('subsplit'); + $this->log('running git-subsplit init ' . $remote); + + $cmd->setArguments(array( + 'init', + $remote + )); + + try { + $output = $cmd->execute(); + } catch (Exception $e) { + throw new BuildException('git subsplit init failed'. $e); + } + $this->log(trim($output), Project::MSG_INFO); + $repo = $this->getRepository(); + chdir($repo . '/.subsplit'); + passthru('php ../composer.phar install --dev'); + chdir($repo); + } + + /** + * Find the composer.json files using Phing's directory scanner + * + * @return array + */ + protected function findSplits() + { + $this->log("checking heads for subsplits"); + $repo = $this->getRepository(); + $base = $this->getBase(); + + $splits = array(); + $heads = $this->getHeads(); + + if (!empty($base)) { + $base = '/' . ltrim($base, '/'); + } else { + $base = '/'; + } + + chdir($repo . '/.subsplit'); + foreach ($heads as $head) { + $splits[$head] = array(); + + // check each head requested *BEFORE* the actual subtree split command gets it + passthru("git checkout '$head'"); + $ds = new DirectoryScanner(); + $ds->setBasedir($repo . '/.subsplit' . $base); + $ds->setIncludes(array('**/'.$this->subIndicatorFile)); + $ds->scan(); + $files = $ds->getIncludedFiles(); + + // Process the files we found + foreach ($files as $file) { + $pkg = file_get_contents($repo . '/.subsplit' . $base .'/'. $file); + $pkg_json = json_decode($pkg, true); + $name = $pkg_json['name']; + $component = str_replace('/composer.json', '', $file); + // keep this for split cmd + $tmpreponame = explode('/', $name); + $reponame = $tmpreponame[1]; + $splits[$head][$component]['repo'] = $reponame; + $nscomponent = str_replace('/', '\\', $component); + $splits[$head][$component]['desc'] = "[READ ONLY] Subtree split of $nscomponent: " . $pkg_json['description']; + } + } + + // go back to how we found it + passthru("git checkout master"); + chdir($repo); + $this->splits = $splits; + } + + /** + * Based on list of repositories we determined we *should* have, talk + * to GitHub and make sure they're all there. + * + */ + protected function verifyRepos() + { + $this->log('verifying GitHub target repos'); + $github_org = $this->getOwningTarget()->getProject()->getProperty('github.org'); + $github_creds = $this->getOwningTarget()->getProject()->getProperty('github.basicauth'); + + if ($github_creds == 'username:password') { + $this->log('Skipping GitHub repo checks. Update github.basicauth in build.properties to verify repos.', 1); + return; + } + + $ch = curl_init('https://api.github.com/orgs/'.$github_org.'/repos?type=all'); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_USERPWD, $github_creds); + // change this when we know we can use our bundled CA bundle! + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + $result = curl_exec($ch); + curl_close($ch); + $repos = json_decode($result, true); + $existing_repos = array(); + + // parse out the repos we found on GitHub + foreach ($repos as $repo) { + $tmpreponame = explode('/', $repo['full_name']); + $reponame = $tmpreponame[1]; + $existing_repos[$reponame] = $repo['description']; + } + + $heads = $this->getHeads(); + foreach ($heads as $head) { + foreach ($this->splits[$head] as $component => $meta) { + + $reponame = $meta['repo']; + + if (!isset($existing_repos[$reponame])) { + $this->log("Creating missing repo $reponame"); + $payload = array( + 'name' => $reponame, + 'description' => $meta['desc'], + 'homepage' => 'http://www.guzzlephp.org/', + 'private' => true, + 'has_issues' => false, + 'has_wiki' => false, + 'has_downloads' => true, + 'auto_init' => false + ); + $ch = curl_init('https://api.github.com/orgs/'.$github_org.'/repos'); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_USERPWD, $github_creds); + curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json')); + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload)); + // change this when we know we can use our bundled CA bundle! + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + $result = curl_exec($ch); + echo "Response code: ".curl_getinfo($ch, CURLINFO_HTTP_CODE)."\n"; + curl_close($ch); + } else { + $this->log("Repo $reponame exists", 2); + } + } + } + } +} diff --git a/guzzle/guzzle/phpunit.xml.dist b/guzzle/guzzle/phpunit.xml.dist new file mode 100644 index 000000000..208fdc08e --- /dev/null +++ b/guzzle/guzzle/phpunit.xml.dist @@ -0,0 +1,48 @@ + + + + + + ./tests/Guzzle/Tests + + + + + + + + + + ./src/Guzzle + + ./src/Guzzle + ./src/Guzzle/Common/Exception/GuzzleException.php + ./src/Guzzle/Http/Exception/HttpException.php + ./src/Guzzle/Http/Exception/ServerErrorResponseException.php + ./src/Guzzle/Http/Exception/ClientErrorResponseException.php + ./src/Guzzle/Http/Exception/TooManyRedirectsException.php + ./src/Guzzle/Http/Exception/CouldNotRewindStreamException.php + ./src/Guzzle/Common/Exception/BadMethodCallException.php + ./src/Guzzle/Common/Exception/InvalidArgumentException.php + ./src/Guzzle/Common/Exception/RuntimeException.php + ./src/Guzzle/Common/Exception/UnexpectedValueException.php + ./src/Guzzle/Service/Exception/ClientNotFoundException.php + ./src/Guzzle/Service/Exception/CommandException.php + ./src/Guzzle/Service/Exception/DescriptionBuilderException.php + ./src/Guzzle/Service/Exception/ServiceBuilderException.php + ./src/Guzzle/Service/Exception/ServiceNotFoundException.php + ./src/Guzzle/Service/Exception/ValidationException.php + ./src/Guzzle/Service/Exception/JsonException.php + + + + + diff --git a/guzzle/guzzle/src/Guzzle/Batch/AbstractBatchDecorator.php b/guzzle/guzzle/src/Guzzle/Batch/AbstractBatchDecorator.php new file mode 100644 index 000000000..0625d71c3 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Batch/AbstractBatchDecorator.php @@ -0,0 +1,66 @@ +decoratedBatch = $decoratedBatch; + } + + /** + * Allow decorators to implement custom methods + * + * @param string $method Missing method name + * @param array $args Method arguments + * + * @return mixed + * @codeCoverageIgnore + */ + public function __call($method, array $args) + { + return call_user_func_array(array($this->decoratedBatch, $method), $args); + } + + public function add($item) + { + $this->decoratedBatch->add($item); + + return $this; + } + + public function flush() + { + return $this->decoratedBatch->flush(); + } + + public function isEmpty() + { + return $this->decoratedBatch->isEmpty(); + } + + /** + * Trace the decorators associated with the batch + * + * @return array + */ + public function getDecorators() + { + $found = array($this); + if (method_exists($this->decoratedBatch, 'getDecorators')) { + $found = array_merge($found, $this->decoratedBatch->getDecorators()); + } + + return $found; + } +} diff --git a/guzzle/guzzle/src/Guzzle/Batch/Batch.php b/guzzle/guzzle/src/Guzzle/Batch/Batch.php new file mode 100644 index 000000000..4d41c54f8 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Batch/Batch.php @@ -0,0 +1,92 @@ +transferStrategy = $transferStrategy; + $this->divisionStrategy = $divisionStrategy; + $this->queue = new \SplQueue(); + $this->queue->setIteratorMode(\SplQueue::IT_MODE_DELETE); + $this->dividedBatches = array(); + } + + public function add($item) + { + $this->queue->enqueue($item); + + return $this; + } + + public function flush() + { + $this->createBatches(); + + $items = array(); + foreach ($this->dividedBatches as $batchIndex => $dividedBatch) { + while ($dividedBatch->valid()) { + $batch = $dividedBatch->current(); + $dividedBatch->next(); + try { + $this->transferStrategy->transfer($batch); + $items = array_merge($items, $batch); + } catch (\Exception $e) { + throw new BatchTransferException($batch, $items, $e, $this->transferStrategy, $this->divisionStrategy); + } + } + // Keep the divided batch down to a minimum in case of a later exception + unset($this->dividedBatches[$batchIndex]); + } + + return $items; + } + + public function isEmpty() + { + return count($this->queue) == 0 && count($this->dividedBatches) == 0; + } + + /** + * Create batches for any queued items + */ + protected function createBatches() + { + if (count($this->queue)) { + if ($batches = $this->divisionStrategy->createBatches($this->queue)) { + // Convert arrays into iterators + if (is_array($batches)) { + $batches = new \ArrayIterator($batches); + } + $this->dividedBatches[] = $batches; + } + } + } +} diff --git a/guzzle/guzzle/src/Guzzle/Batch/BatchBuilder.php b/guzzle/guzzle/src/Guzzle/Batch/BatchBuilder.php new file mode 100644 index 000000000..ea99b4dd0 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Batch/BatchBuilder.php @@ -0,0 +1,199 @@ + 'Guzzle\Batch\BatchRequestTransfer', + 'command' => 'Guzzle\Batch\BatchCommandTransfer' + ); + + /** + * Create a new instance of the BatchBuilder + * + * @return BatchBuilder + */ + public static function factory() + { + return new self(); + } + + /** + * Automatically flush the batch when the size of the queue reaches a certain threshold. Adds {@see FlushingBatch}. + * + * @param $threshold Number of items to allow in the queue before a flush + * + * @return BatchBuilder + */ + public function autoFlushAt($threshold) + { + $this->autoFlush = $threshold; + + return $this; + } + + /** + * Maintain a history of all items that have been transferred using the batch. Adds {@see HistoryBatch}. + * + * @return BatchBuilder + */ + public function keepHistory() + { + $this->history = true; + + return $this; + } + + /** + * Buffer exceptions thrown during transfer so that you can transfer as much as possible, and after a transfer + * completes, inspect each exception that was thrown. Enables the {@see ExceptionBufferingBatch} decorator. + * + * @return BatchBuilder + */ + public function bufferExceptions() + { + $this->exceptionBuffering = true; + + return $this; + } + + /** + * Notify a callable each time a batch flush completes. Enables the {@see NotifyingBatch} decorator. + * + * @param mixed $callable Callable function to notify + * + * @return BatchBuilder + * @throws InvalidArgumentException if the argument is not callable + */ + public function notify($callable) + { + $this->afterFlush = $callable; + + return $this; + } + + /** + * Configures the batch to transfer batches of requests. Associates a {@see \Guzzle\Http\BatchRequestTransfer} + * object as both the transfer and divisor strategy. + * + * @param int $batchSize Batch size for each batch of requests + * + * @return BatchBuilder + */ + public function transferRequests($batchSize = 50) + { + $className = self::$mapping['request']; + $this->transferStrategy = new $className($batchSize); + $this->divisorStrategy = $this->transferStrategy; + + return $this; + } + + /** + * Configures the batch to transfer batches commands. Associates as + * {@see \Guzzle\Service\Command\BatchCommandTransfer} as both the transfer and divisor strategy. + * + * @param int $batchSize Batch size for each batch of commands + * + * @return BatchBuilder + */ + public function transferCommands($batchSize = 50) + { + $className = self::$mapping['command']; + $this->transferStrategy = new $className($batchSize); + $this->divisorStrategy = $this->transferStrategy; + + return $this; + } + + /** + * Specify the strategy used to divide the queue into an array of batches + * + * @param BatchDivisorInterface $divisorStrategy Strategy used to divide a batch queue into batches + * + * @return BatchBuilder + */ + public function createBatchesWith(BatchDivisorInterface $divisorStrategy) + { + $this->divisorStrategy = $divisorStrategy; + + return $this; + } + + /** + * Specify the strategy used to transport the items when flush is called + * + * @param BatchTransferInterface $transferStrategy How items are transferred + * + * @return BatchBuilder + */ + public function transferWith(BatchTransferInterface $transferStrategy) + { + $this->transferStrategy = $transferStrategy; + + return $this; + } + + /** + * Create and return the instantiated batch + * + * @return BatchInterface + * @throws RuntimeException if no transfer strategy has been specified + */ + public function build() + { + if (!$this->transferStrategy) { + throw new RuntimeException('No transfer strategy has been specified'); + } + + if (!$this->divisorStrategy) { + throw new RuntimeException('No divisor strategy has been specified'); + } + + $batch = new Batch($this->transferStrategy, $this->divisorStrategy); + + if ($this->exceptionBuffering) { + $batch = new ExceptionBufferingBatch($batch); + } + + if ($this->afterFlush) { + $batch = new NotifyingBatch($batch, $this->afterFlush); + } + + if ($this->autoFlush) { + $batch = new FlushingBatch($batch, $this->autoFlush); + } + + if ($this->history) { + $batch = new HistoryBatch($batch); + } + + return $batch; + } +} diff --git a/guzzle/guzzle/src/Guzzle/Batch/BatchClosureDivisor.php b/guzzle/guzzle/src/Guzzle/Batch/BatchClosureDivisor.php new file mode 100644 index 000000000..e0a2d9568 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Batch/BatchClosureDivisor.php @@ -0,0 +1,39 @@ +callable = $callable; + $this->context = $context; + } + + public function createBatches(\SplQueue $queue) + { + return call_user_func($this->callable, $queue, $this->context); + } +} diff --git a/guzzle/guzzle/src/Guzzle/Batch/BatchClosureTransfer.php b/guzzle/guzzle/src/Guzzle/Batch/BatchClosureTransfer.php new file mode 100644 index 000000000..9cbf1aba4 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Batch/BatchClosureTransfer.php @@ -0,0 +1,40 @@ +callable = $callable; + $this->context = $context; + } + + public function transfer(array $batch) + { + return empty($batch) ? null : call_user_func($this->callable, $batch, $this->context); + } +} diff --git a/guzzle/guzzle/src/Guzzle/Batch/BatchCommandTransfer.php b/guzzle/guzzle/src/Guzzle/Batch/BatchCommandTransfer.php new file mode 100644 index 000000000..d55ac7d1f --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Batch/BatchCommandTransfer.php @@ -0,0 +1,75 @@ +batchSize = $batchSize; + } + + /** + * Creates batches by grouping commands by their associated client + * {@inheritdoc} + */ + public function createBatches(\SplQueue $queue) + { + $groups = new \SplObjectStorage(); + foreach ($queue as $item) { + if (!$item instanceof CommandInterface) { + throw new InvalidArgumentException('All items must implement Guzzle\Service\Command\CommandInterface'); + } + $client = $item->getClient(); + if (!$groups->contains($client)) { + $groups->attach($client, new \ArrayObject(array($item))); + } else { + $groups[$client]->append($item); + } + } + + $batches = array(); + foreach ($groups as $batch) { + $batches = array_merge($batches, array_chunk($groups[$batch]->getArrayCopy(), $this->batchSize)); + } + + return $batches; + } + + public function transfer(array $batch) + { + if (empty($batch)) { + return; + } + + // Get the client of the first found command + $client = reset($batch)->getClient(); + + // Keep a list of all commands with invalid clients + $invalid = array_filter($batch, function ($command) use ($client) { + return $command->getClient() !== $client; + }); + + if (!empty($invalid)) { + throw new InconsistentClientTransferException($invalid); + } + + $client->execute($batch); + } +} diff --git a/guzzle/guzzle/src/Guzzle/Batch/BatchDivisorInterface.php b/guzzle/guzzle/src/Guzzle/Batch/BatchDivisorInterface.php new file mode 100644 index 000000000..0214f05f4 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Batch/BatchDivisorInterface.php @@ -0,0 +1,18 @@ +batchSize = $batchSize; + } + + /** + * Creates batches of requests by grouping requests by their associated curl multi object. + * {@inheritdoc} + */ + public function createBatches(\SplQueue $queue) + { + // Create batches by client objects + $groups = new \SplObjectStorage(); + foreach ($queue as $item) { + if (!$item instanceof RequestInterface) { + throw new InvalidArgumentException('All items must implement Guzzle\Http\Message\RequestInterface'); + } + $client = $item->getClient(); + if (!$groups->contains($client)) { + $groups->attach($client, array($item)); + } else { + $current = $groups[$client]; + $current[] = $item; + $groups[$client] = $current; + } + } + + $batches = array(); + foreach ($groups as $batch) { + $batches = array_merge($batches, array_chunk($groups[$batch], $this->batchSize)); + } + + return $batches; + } + + public function transfer(array $batch) + { + if ($batch) { + reset($batch)->getClient()->send($batch); + } + } +} diff --git a/guzzle/guzzle/src/Guzzle/Batch/BatchSizeDivisor.php b/guzzle/guzzle/src/Guzzle/Batch/BatchSizeDivisor.php new file mode 100644 index 000000000..67f90a581 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Batch/BatchSizeDivisor.php @@ -0,0 +1,47 @@ +size = $size; + } + + /** + * Set the size of each batch + * + * @param int $size Size of each batch + * + * @return BatchSizeDivisor + */ + public function setSize($size) + { + $this->size = $size; + + return $this; + } + + /** + * Get the size of each batch + * + * @return int + */ + public function getSize() + { + return $this->size; + } + + public function createBatches(\SplQueue $queue) + { + return array_chunk(iterator_to_array($queue, false), $this->size); + } +} diff --git a/guzzle/guzzle/src/Guzzle/Batch/BatchTransferInterface.php b/guzzle/guzzle/src/Guzzle/Batch/BatchTransferInterface.php new file mode 100644 index 000000000..2e0b60dad --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Batch/BatchTransferInterface.php @@ -0,0 +1,16 @@ +batch = $batch; + $this->transferredItems = $transferredItems; + $this->transferStrategy = $transferStrategy; + $this->divisorStrategy = $divisorStrategy; + parent::__construct( + 'Exception encountered while transferring batch: ' . $exception->getMessage(), + $exception->getCode(), + $exception + ); + } + + /** + * Get the batch that we being sent when the exception occurred + * + * @return array + */ + public function getBatch() + { + return $this->batch; + } + + /** + * Get the items transferred at the point in which the exception was encountered + * + * @return array + */ + public function getTransferredItems() + { + return $this->transferredItems; + } + + /** + * Get the transfer strategy + * + * @return TransferStrategy + */ + public function getTransferStrategy() + { + return $this->transferStrategy; + } + + /** + * Get the divisor strategy + * + * @return DivisorStrategy + */ + public function getDivisorStrategy() + { + return $this->divisorStrategy; + } +} diff --git a/guzzle/guzzle/src/Guzzle/Batch/ExceptionBufferingBatch.php b/guzzle/guzzle/src/Guzzle/Batch/ExceptionBufferingBatch.php new file mode 100644 index 000000000..d7a892885 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Batch/ExceptionBufferingBatch.php @@ -0,0 +1,50 @@ +decoratedBatch->isEmpty()) { + try { + $transferredItems = $this->decoratedBatch->flush(); + } catch (BatchTransferException $e) { + $this->exceptions[] = $e; + $transferredItems = $e->getTransferredItems(); + } + $items = array_merge($items, $transferredItems); + } + + return $items; + } + + /** + * Get the buffered exceptions + * + * @return array Array of BatchTransferException objects + */ + public function getExceptions() + { + return $this->exceptions; + } + + /** + * Clear the buffered exceptions + */ + public function clearExceptions() + { + $this->exceptions = array(); + } +} diff --git a/guzzle/guzzle/src/Guzzle/Batch/FlushingBatch.php b/guzzle/guzzle/src/Guzzle/Batch/FlushingBatch.php new file mode 100644 index 000000000..367b68427 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Batch/FlushingBatch.php @@ -0,0 +1,60 @@ +threshold = $threshold; + parent::__construct($decoratedBatch); + } + + /** + * Set the auto-flush threshold + * + * @param int $threshold The auto-flush threshold + * + * @return FlushingBatch + */ + public function setThreshold($threshold) + { + $this->threshold = $threshold; + + return $this; + } + + /** + * Get the auto-flush threshold + * + * @return int + */ + public function getThreshold() + { + return $this->threshold; + } + + public function add($item) + { + $this->decoratedBatch->add($item); + if (++$this->currentTotal >= $this->threshold) { + $this->currentTotal = 0; + $this->decoratedBatch->flush(); + } + + return $this; + } +} diff --git a/guzzle/guzzle/src/Guzzle/Batch/HistoryBatch.php b/guzzle/guzzle/src/Guzzle/Batch/HistoryBatch.php new file mode 100644 index 000000000..e345fdc34 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Batch/HistoryBatch.php @@ -0,0 +1,39 @@ +history[] = $item; + $this->decoratedBatch->add($item); + + return $this; + } + + /** + * Get the batch history + * + * @return array + */ + public function getHistory() + { + return $this->history; + } + + /** + * Clear the batch history + */ + public function clearHistory() + { + $this->history = array(); + } +} diff --git a/guzzle/guzzle/src/Guzzle/Batch/NotifyingBatch.php b/guzzle/guzzle/src/Guzzle/Batch/NotifyingBatch.php new file mode 100644 index 000000000..96d04daa8 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Batch/NotifyingBatch.php @@ -0,0 +1,38 @@ +callable = $callable; + parent::__construct($decoratedBatch); + } + + public function flush() + { + $items = $this->decoratedBatch->flush(); + call_user_func($this->callable, $items); + + return $items; + } +} diff --git a/guzzle/guzzle/src/Guzzle/Batch/composer.json b/guzzle/guzzle/src/Guzzle/Batch/composer.json new file mode 100644 index 000000000..12404d381 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Batch/composer.json @@ -0,0 +1,31 @@ +{ + "name": "guzzle/batch", + "description": "Guzzle batch component for batching requests, commands, or custom transfers", + "homepage": "http://guzzlephp.org/", + "keywords": ["batch", "HTTP", "REST", "guzzle"], + "license": "MIT", + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "require": { + "php": ">=5.3.2", + "guzzle/common": "self.version" + }, + "autoload": { + "psr-0": { "Guzzle\\Batch": "" } + }, + "suggest": { + "guzzle/http": "self.version", + "guzzle/service": "self.version" + }, + "target-dir": "Guzzle/Batch", + "extra": { + "branch-alias": { + "dev-master": "3.7-dev" + } + } +} diff --git a/guzzle/guzzle/src/Guzzle/Cache/AbstractCacheAdapter.php b/guzzle/guzzle/src/Guzzle/Cache/AbstractCacheAdapter.php new file mode 100644 index 000000000..a5c527167 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Cache/AbstractCacheAdapter.php @@ -0,0 +1,21 @@ +cache; + } +} diff --git a/guzzle/guzzle/src/Guzzle/Cache/CacheAdapterFactory.php b/guzzle/guzzle/src/Guzzle/Cache/CacheAdapterFactory.php new file mode 100644 index 000000000..94e623463 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Cache/CacheAdapterFactory.php @@ -0,0 +1,117 @@ +newInstanceArgs($args); + } + } catch (\Exception $e) { + throw new RuntimeException($e->getMessage(), $e->getCode(), $e); + } + } +} diff --git a/guzzle/guzzle/src/Guzzle/Cache/CacheAdapterInterface.php b/guzzle/guzzle/src/Guzzle/Cache/CacheAdapterInterface.php new file mode 100644 index 000000000..970c9e228 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Cache/CacheAdapterInterface.php @@ -0,0 +1,55 @@ +callables = $callables; + } + + public function contains($id, array $options = null) + { + return call_user_func($this->callables['contains'], $id, $options); + } + + public function delete($id, array $options = null) + { + return call_user_func($this->callables['delete'], $id, $options); + } + + public function fetch($id, array $options = null) + { + return call_user_func($this->callables['fetch'], $id, $options); + } + + public function save($id, $data, $lifeTime = false, array $options = null) + { + return call_user_func($this->callables['save'], $id, $data, $lifeTime, $options); + } +} diff --git a/guzzle/guzzle/src/Guzzle/Cache/DoctrineCacheAdapter.php b/guzzle/guzzle/src/Guzzle/Cache/DoctrineCacheAdapter.php new file mode 100644 index 000000000..321dd6baf --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Cache/DoctrineCacheAdapter.php @@ -0,0 +1,41 @@ +cache = $cache; + } + + public function contains($id, array $options = null) + { + return $this->cache->contains($id); + } + + public function delete($id, array $options = null) + { + return $this->cache->delete($id); + } + + public function fetch($id, array $options = null) + { + return $this->cache->fetch($id); + } + + public function save($id, $data, $lifeTime = false, array $options = null) + { + return $this->cache->save($id, $data, $lifeTime); + } +} diff --git a/guzzle/guzzle/src/Guzzle/Cache/NullCacheAdapter.php b/guzzle/guzzle/src/Guzzle/Cache/NullCacheAdapter.php new file mode 100644 index 000000000..68bd4af97 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Cache/NullCacheAdapter.php @@ -0,0 +1,31 @@ +cache = $cache; + } + + public function contains($id, array $options = null) + { + return $this->cache->test($id); + } + + public function delete($id, array $options = null) + { + return $this->cache->remove($id); + } + + public function fetch($id, array $options = null) + { + return $this->cache->load($id); + } + + public function save($id, $data, $lifeTime = false, array $options = null) + { + return $this->cache->save($data, $id, array(), $lifeTime); + } +} diff --git a/guzzle/guzzle/src/Guzzle/Cache/Zf2CacheAdapter.php b/guzzle/guzzle/src/Guzzle/Cache/Zf2CacheAdapter.php new file mode 100644 index 000000000..1fc18a555 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Cache/Zf2CacheAdapter.php @@ -0,0 +1,41 @@ +cache = $cache; + } + + public function contains($id, array $options = null) + { + return $this->cache->hasItem($id); + } + + public function delete($id, array $options = null) + { + return $this->cache->removeItem($id); + } + + public function fetch($id, array $options = null) + { + return $this->cache->getItem($id); + } + + public function save($id, $data, $lifeTime = false, array $options = null) + { + return $this->cache->setItem($id, $data); + } +} diff --git a/guzzle/guzzle/src/Guzzle/Cache/composer.json b/guzzle/guzzle/src/Guzzle/Cache/composer.json new file mode 100644 index 000000000..a5d999bd6 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Cache/composer.json @@ -0,0 +1,27 @@ +{ + "name": "guzzle/cache", + "description": "Guzzle cache adapter component", + "homepage": "http://guzzlephp.org/", + "keywords": ["cache", "adapter", "zf", "doctrine", "guzzle"], + "license": "MIT", + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "require": { + "php": ">=5.3.2", + "guzzle/common": "self.version" + }, + "autoload": { + "psr-0": { "Guzzle\\Cache": "" } + }, + "target-dir": "Guzzle/Cache", + "extra": { + "branch-alias": { + "dev-master": "3.7-dev" + } + } +} diff --git a/guzzle/common/Guzzle/Common/AbstractHasDispatcher.php b/guzzle/guzzle/src/Guzzle/Common/AbstractHasDispatcher.php similarity index 100% rename from guzzle/common/Guzzle/Common/AbstractHasDispatcher.php rename to guzzle/guzzle/src/Guzzle/Common/AbstractHasDispatcher.php diff --git a/guzzle/common/Guzzle/Common/Collection.php b/guzzle/guzzle/src/Guzzle/Common/Collection.php similarity index 100% rename from guzzle/common/Guzzle/Common/Collection.php rename to guzzle/guzzle/src/Guzzle/Common/Collection.php diff --git a/guzzle/common/Guzzle/Common/Event.php b/guzzle/guzzle/src/Guzzle/Common/Event.php similarity index 100% rename from guzzle/common/Guzzle/Common/Event.php rename to guzzle/guzzle/src/Guzzle/Common/Event.php diff --git a/guzzle/common/Guzzle/Common/Exception/BadMethodCallException.php b/guzzle/guzzle/src/Guzzle/Common/Exception/BadMethodCallException.php similarity index 100% rename from guzzle/common/Guzzle/Common/Exception/BadMethodCallException.php rename to guzzle/guzzle/src/Guzzle/Common/Exception/BadMethodCallException.php diff --git a/guzzle/common/Guzzle/Common/Exception/ExceptionCollection.php b/guzzle/guzzle/src/Guzzle/Common/Exception/ExceptionCollection.php similarity index 100% rename from guzzle/common/Guzzle/Common/Exception/ExceptionCollection.php rename to guzzle/guzzle/src/Guzzle/Common/Exception/ExceptionCollection.php diff --git a/guzzle/common/Guzzle/Common/Exception/GuzzleException.php b/guzzle/guzzle/src/Guzzle/Common/Exception/GuzzleException.php similarity index 100% rename from guzzle/common/Guzzle/Common/Exception/GuzzleException.php rename to guzzle/guzzle/src/Guzzle/Common/Exception/GuzzleException.php diff --git a/guzzle/common/Guzzle/Common/Exception/InvalidArgumentException.php b/guzzle/guzzle/src/Guzzle/Common/Exception/InvalidArgumentException.php similarity index 100% rename from guzzle/common/Guzzle/Common/Exception/InvalidArgumentException.php rename to guzzle/guzzle/src/Guzzle/Common/Exception/InvalidArgumentException.php diff --git a/guzzle/common/Guzzle/Common/Exception/RuntimeException.php b/guzzle/guzzle/src/Guzzle/Common/Exception/RuntimeException.php similarity index 100% rename from guzzle/common/Guzzle/Common/Exception/RuntimeException.php rename to guzzle/guzzle/src/Guzzle/Common/Exception/RuntimeException.php diff --git a/guzzle/common/Guzzle/Common/Exception/UnexpectedValueException.php b/guzzle/guzzle/src/Guzzle/Common/Exception/UnexpectedValueException.php similarity index 100% rename from guzzle/common/Guzzle/Common/Exception/UnexpectedValueException.php rename to guzzle/guzzle/src/Guzzle/Common/Exception/UnexpectedValueException.php diff --git a/guzzle/common/Guzzle/Common/FromConfigInterface.php b/guzzle/guzzle/src/Guzzle/Common/FromConfigInterface.php similarity index 100% rename from guzzle/common/Guzzle/Common/FromConfigInterface.php rename to guzzle/guzzle/src/Guzzle/Common/FromConfigInterface.php diff --git a/guzzle/common/Guzzle/Common/HasDispatcherInterface.php b/guzzle/guzzle/src/Guzzle/Common/HasDispatcherInterface.php similarity index 100% rename from guzzle/common/Guzzle/Common/HasDispatcherInterface.php rename to guzzle/guzzle/src/Guzzle/Common/HasDispatcherInterface.php diff --git a/guzzle/common/Guzzle/Common/ToArrayInterface.php b/guzzle/guzzle/src/Guzzle/Common/ToArrayInterface.php similarity index 100% rename from guzzle/common/Guzzle/Common/ToArrayInterface.php rename to guzzle/guzzle/src/Guzzle/Common/ToArrayInterface.php diff --git a/guzzle/common/Guzzle/Common/Version.php b/guzzle/guzzle/src/Guzzle/Common/Version.php similarity index 100% rename from guzzle/common/Guzzle/Common/Version.php rename to guzzle/guzzle/src/Guzzle/Common/Version.php diff --git a/guzzle/common/Guzzle/Common/composer.json b/guzzle/guzzle/src/Guzzle/Common/composer.json similarity index 100% rename from guzzle/common/Guzzle/Common/composer.json rename to guzzle/guzzle/src/Guzzle/Common/composer.json diff --git a/guzzle/http/Guzzle/Http/AbstractEntityBodyDecorator.php b/guzzle/guzzle/src/Guzzle/Http/AbstractEntityBodyDecorator.php similarity index 100% rename from guzzle/http/Guzzle/Http/AbstractEntityBodyDecorator.php rename to guzzle/guzzle/src/Guzzle/Http/AbstractEntityBodyDecorator.php diff --git a/guzzle/http/Guzzle/Http/CachingEntityBody.php b/guzzle/guzzle/src/Guzzle/Http/CachingEntityBody.php similarity index 100% rename from guzzle/http/Guzzle/Http/CachingEntityBody.php rename to guzzle/guzzle/src/Guzzle/Http/CachingEntityBody.php diff --git a/guzzle/http/Guzzle/Http/Client.php b/guzzle/guzzle/src/Guzzle/Http/Client.php similarity index 100% rename from guzzle/http/Guzzle/Http/Client.php rename to guzzle/guzzle/src/Guzzle/Http/Client.php diff --git a/guzzle/http/Guzzle/Http/ClientInterface.php b/guzzle/guzzle/src/Guzzle/Http/ClientInterface.php similarity index 100% rename from guzzle/http/Guzzle/Http/ClientInterface.php rename to guzzle/guzzle/src/Guzzle/Http/ClientInterface.php diff --git a/guzzle/http/Guzzle/Http/Curl/CurlHandle.php b/guzzle/guzzle/src/Guzzle/Http/Curl/CurlHandle.php similarity index 100% rename from guzzle/http/Guzzle/Http/Curl/CurlHandle.php rename to guzzle/guzzle/src/Guzzle/Http/Curl/CurlHandle.php diff --git a/guzzle/http/Guzzle/Http/Curl/CurlMulti.php b/guzzle/guzzle/src/Guzzle/Http/Curl/CurlMulti.php similarity index 100% rename from guzzle/http/Guzzle/Http/Curl/CurlMulti.php rename to guzzle/guzzle/src/Guzzle/Http/Curl/CurlMulti.php diff --git a/guzzle/http/Guzzle/Http/Curl/CurlMultiInterface.php b/guzzle/guzzle/src/Guzzle/Http/Curl/CurlMultiInterface.php similarity index 100% rename from guzzle/http/Guzzle/Http/Curl/CurlMultiInterface.php rename to guzzle/guzzle/src/Guzzle/Http/Curl/CurlMultiInterface.php diff --git a/guzzle/http/Guzzle/Http/Curl/CurlMultiProxy.php b/guzzle/guzzle/src/Guzzle/Http/Curl/CurlMultiProxy.php similarity index 100% rename from guzzle/http/Guzzle/Http/Curl/CurlMultiProxy.php rename to guzzle/guzzle/src/Guzzle/Http/Curl/CurlMultiProxy.php diff --git a/guzzle/http/Guzzle/Http/Curl/CurlVersion.php b/guzzle/guzzle/src/Guzzle/Http/Curl/CurlVersion.php similarity index 100% rename from guzzle/http/Guzzle/Http/Curl/CurlVersion.php rename to guzzle/guzzle/src/Guzzle/Http/Curl/CurlVersion.php diff --git a/guzzle/http/Guzzle/Http/Curl/RequestMediator.php b/guzzle/guzzle/src/Guzzle/Http/Curl/RequestMediator.php similarity index 100% rename from guzzle/http/Guzzle/Http/Curl/RequestMediator.php rename to guzzle/guzzle/src/Guzzle/Http/Curl/RequestMediator.php diff --git a/guzzle/http/Guzzle/Http/EntityBody.php b/guzzle/guzzle/src/Guzzle/Http/EntityBody.php similarity index 100% rename from guzzle/http/Guzzle/Http/EntityBody.php rename to guzzle/guzzle/src/Guzzle/Http/EntityBody.php diff --git a/guzzle/http/Guzzle/Http/EntityBodyInterface.php b/guzzle/guzzle/src/Guzzle/Http/EntityBodyInterface.php similarity index 100% rename from guzzle/http/Guzzle/Http/EntityBodyInterface.php rename to guzzle/guzzle/src/Guzzle/Http/EntityBodyInterface.php diff --git a/guzzle/http/Guzzle/Http/Exception/BadResponseException.php b/guzzle/guzzle/src/Guzzle/Http/Exception/BadResponseException.php similarity index 100% rename from guzzle/http/Guzzle/Http/Exception/BadResponseException.php rename to guzzle/guzzle/src/Guzzle/Http/Exception/BadResponseException.php diff --git a/guzzle/http/Guzzle/Http/Exception/ClientErrorResponseException.php b/guzzle/guzzle/src/Guzzle/Http/Exception/ClientErrorResponseException.php similarity index 100% rename from guzzle/http/Guzzle/Http/Exception/ClientErrorResponseException.php rename to guzzle/guzzle/src/Guzzle/Http/Exception/ClientErrorResponseException.php diff --git a/guzzle/http/Guzzle/Http/Exception/CouldNotRewindStreamException.php b/guzzle/guzzle/src/Guzzle/Http/Exception/CouldNotRewindStreamException.php similarity index 100% rename from guzzle/http/Guzzle/Http/Exception/CouldNotRewindStreamException.php rename to guzzle/guzzle/src/Guzzle/Http/Exception/CouldNotRewindStreamException.php diff --git a/guzzle/http/Guzzle/Http/Exception/CurlException.php b/guzzle/guzzle/src/Guzzle/Http/Exception/CurlException.php similarity index 100% rename from guzzle/http/Guzzle/Http/Exception/CurlException.php rename to guzzle/guzzle/src/Guzzle/Http/Exception/CurlException.php diff --git a/guzzle/http/Guzzle/Http/Exception/HttpException.php b/guzzle/guzzle/src/Guzzle/Http/Exception/HttpException.php similarity index 100% rename from guzzle/http/Guzzle/Http/Exception/HttpException.php rename to guzzle/guzzle/src/Guzzle/Http/Exception/HttpException.php diff --git a/guzzle/http/Guzzle/Http/Exception/MultiTransferException.php b/guzzle/guzzle/src/Guzzle/Http/Exception/MultiTransferException.php similarity index 100% rename from guzzle/http/Guzzle/Http/Exception/MultiTransferException.php rename to guzzle/guzzle/src/Guzzle/Http/Exception/MultiTransferException.php diff --git a/guzzle/http/Guzzle/Http/Exception/RequestException.php b/guzzle/guzzle/src/Guzzle/Http/Exception/RequestException.php similarity index 100% rename from guzzle/http/Guzzle/Http/Exception/RequestException.php rename to guzzle/guzzle/src/Guzzle/Http/Exception/RequestException.php diff --git a/guzzle/http/Guzzle/Http/Exception/ServerErrorResponseException.php b/guzzle/guzzle/src/Guzzle/Http/Exception/ServerErrorResponseException.php similarity index 100% rename from guzzle/http/Guzzle/Http/Exception/ServerErrorResponseException.php rename to guzzle/guzzle/src/Guzzle/Http/Exception/ServerErrorResponseException.php diff --git a/guzzle/http/Guzzle/Http/Exception/TooManyRedirectsException.php b/guzzle/guzzle/src/Guzzle/Http/Exception/TooManyRedirectsException.php similarity index 100% rename from guzzle/http/Guzzle/Http/Exception/TooManyRedirectsException.php rename to guzzle/guzzle/src/Guzzle/Http/Exception/TooManyRedirectsException.php diff --git a/guzzle/http/Guzzle/Http/IoEmittingEntityBody.php b/guzzle/guzzle/src/Guzzle/Http/IoEmittingEntityBody.php similarity index 100% rename from guzzle/http/Guzzle/Http/IoEmittingEntityBody.php rename to guzzle/guzzle/src/Guzzle/Http/IoEmittingEntityBody.php diff --git a/guzzle/http/Guzzle/Http/Message/AbstractMessage.php b/guzzle/guzzle/src/Guzzle/Http/Message/AbstractMessage.php similarity index 100% rename from guzzle/http/Guzzle/Http/Message/AbstractMessage.php rename to guzzle/guzzle/src/Guzzle/Http/Message/AbstractMessage.php diff --git a/guzzle/http/Guzzle/Http/Message/EntityEnclosingRequest.php b/guzzle/guzzle/src/Guzzle/Http/Message/EntityEnclosingRequest.php similarity index 100% rename from guzzle/http/Guzzle/Http/Message/EntityEnclosingRequest.php rename to guzzle/guzzle/src/Guzzle/Http/Message/EntityEnclosingRequest.php diff --git a/guzzle/http/Guzzle/Http/Message/EntityEnclosingRequestInterface.php b/guzzle/guzzle/src/Guzzle/Http/Message/EntityEnclosingRequestInterface.php similarity index 100% rename from guzzle/http/Guzzle/Http/Message/EntityEnclosingRequestInterface.php rename to guzzle/guzzle/src/Guzzle/Http/Message/EntityEnclosingRequestInterface.php diff --git a/guzzle/http/Guzzle/Http/Message/Header.php b/guzzle/guzzle/src/Guzzle/Http/Message/Header.php similarity index 100% rename from guzzle/http/Guzzle/Http/Message/Header.php rename to guzzle/guzzle/src/Guzzle/Http/Message/Header.php diff --git a/guzzle/http/Guzzle/Http/Message/Header/CacheControl.php b/guzzle/guzzle/src/Guzzle/Http/Message/Header/CacheControl.php similarity index 100% rename from guzzle/http/Guzzle/Http/Message/Header/CacheControl.php rename to guzzle/guzzle/src/Guzzle/Http/Message/Header/CacheControl.php diff --git a/guzzle/http/Guzzle/Http/Message/Header/HeaderCollection.php b/guzzle/guzzle/src/Guzzle/Http/Message/Header/HeaderCollection.php similarity index 100% rename from guzzle/http/Guzzle/Http/Message/Header/HeaderCollection.php rename to guzzle/guzzle/src/Guzzle/Http/Message/Header/HeaderCollection.php diff --git a/guzzle/http/Guzzle/Http/Message/Header/HeaderFactory.php b/guzzle/guzzle/src/Guzzle/Http/Message/Header/HeaderFactory.php similarity index 100% rename from guzzle/http/Guzzle/Http/Message/Header/HeaderFactory.php rename to guzzle/guzzle/src/Guzzle/Http/Message/Header/HeaderFactory.php diff --git a/guzzle/http/Guzzle/Http/Message/Header/HeaderFactoryInterface.php b/guzzle/guzzle/src/Guzzle/Http/Message/Header/HeaderFactoryInterface.php similarity index 100% rename from guzzle/http/Guzzle/Http/Message/Header/HeaderFactoryInterface.php rename to guzzle/guzzle/src/Guzzle/Http/Message/Header/HeaderFactoryInterface.php diff --git a/guzzle/http/Guzzle/Http/Message/Header/HeaderInterface.php b/guzzle/guzzle/src/Guzzle/Http/Message/Header/HeaderInterface.php similarity index 100% rename from guzzle/http/Guzzle/Http/Message/Header/HeaderInterface.php rename to guzzle/guzzle/src/Guzzle/Http/Message/Header/HeaderInterface.php diff --git a/guzzle/http/Guzzle/Http/Message/Header/Link.php b/guzzle/guzzle/src/Guzzle/Http/Message/Header/Link.php similarity index 100% rename from guzzle/http/Guzzle/Http/Message/Header/Link.php rename to guzzle/guzzle/src/Guzzle/Http/Message/Header/Link.php diff --git a/guzzle/http/Guzzle/Http/Message/MessageInterface.php b/guzzle/guzzle/src/Guzzle/Http/Message/MessageInterface.php similarity index 100% rename from guzzle/http/Guzzle/Http/Message/MessageInterface.php rename to guzzle/guzzle/src/Guzzle/Http/Message/MessageInterface.php diff --git a/guzzle/http/Guzzle/Http/Message/PostFile.php b/guzzle/guzzle/src/Guzzle/Http/Message/PostFile.php similarity index 100% rename from guzzle/http/Guzzle/Http/Message/PostFile.php rename to guzzle/guzzle/src/Guzzle/Http/Message/PostFile.php diff --git a/guzzle/http/Guzzle/Http/Message/PostFileInterface.php b/guzzle/guzzle/src/Guzzle/Http/Message/PostFileInterface.php similarity index 100% rename from guzzle/http/Guzzle/Http/Message/PostFileInterface.php rename to guzzle/guzzle/src/Guzzle/Http/Message/PostFileInterface.php diff --git a/guzzle/http/Guzzle/Http/Message/Request.php b/guzzle/guzzle/src/Guzzle/Http/Message/Request.php similarity index 100% rename from guzzle/http/Guzzle/Http/Message/Request.php rename to guzzle/guzzle/src/Guzzle/Http/Message/Request.php diff --git a/guzzle/http/Guzzle/Http/Message/RequestFactory.php b/guzzle/guzzle/src/Guzzle/Http/Message/RequestFactory.php similarity index 100% rename from guzzle/http/Guzzle/Http/Message/RequestFactory.php rename to guzzle/guzzle/src/Guzzle/Http/Message/RequestFactory.php diff --git a/guzzle/http/Guzzle/Http/Message/RequestFactoryInterface.php b/guzzle/guzzle/src/Guzzle/Http/Message/RequestFactoryInterface.php similarity index 100% rename from guzzle/http/Guzzle/Http/Message/RequestFactoryInterface.php rename to guzzle/guzzle/src/Guzzle/Http/Message/RequestFactoryInterface.php diff --git a/guzzle/http/Guzzle/Http/Message/RequestInterface.php b/guzzle/guzzle/src/Guzzle/Http/Message/RequestInterface.php similarity index 100% rename from guzzle/http/Guzzle/Http/Message/RequestInterface.php rename to guzzle/guzzle/src/Guzzle/Http/Message/RequestInterface.php diff --git a/guzzle/http/Guzzle/Http/Message/Response.php b/guzzle/guzzle/src/Guzzle/Http/Message/Response.php similarity index 100% rename from guzzle/http/Guzzle/Http/Message/Response.php rename to guzzle/guzzle/src/Guzzle/Http/Message/Response.php diff --git a/guzzle/http/Guzzle/Http/Mimetypes.php b/guzzle/guzzle/src/Guzzle/Http/Mimetypes.php similarity index 100% rename from guzzle/http/Guzzle/Http/Mimetypes.php rename to guzzle/guzzle/src/Guzzle/Http/Mimetypes.php diff --git a/guzzle/http/Guzzle/Http/QueryAggregator/CommaAggregator.php b/guzzle/guzzle/src/Guzzle/Http/QueryAggregator/CommaAggregator.php similarity index 100% rename from guzzle/http/Guzzle/Http/QueryAggregator/CommaAggregator.php rename to guzzle/guzzle/src/Guzzle/Http/QueryAggregator/CommaAggregator.php diff --git a/guzzle/http/Guzzle/Http/QueryAggregator/DuplicateAggregator.php b/guzzle/guzzle/src/Guzzle/Http/QueryAggregator/DuplicateAggregator.php similarity index 100% rename from guzzle/http/Guzzle/Http/QueryAggregator/DuplicateAggregator.php rename to guzzle/guzzle/src/Guzzle/Http/QueryAggregator/DuplicateAggregator.php diff --git a/guzzle/http/Guzzle/Http/QueryAggregator/PhpAggregator.php b/guzzle/guzzle/src/Guzzle/Http/QueryAggregator/PhpAggregator.php similarity index 100% rename from guzzle/http/Guzzle/Http/QueryAggregator/PhpAggregator.php rename to guzzle/guzzle/src/Guzzle/Http/QueryAggregator/PhpAggregator.php diff --git a/guzzle/http/Guzzle/Http/QueryAggregator/QueryAggregatorInterface.php b/guzzle/guzzle/src/Guzzle/Http/QueryAggregator/QueryAggregatorInterface.php similarity index 100% rename from guzzle/http/Guzzle/Http/QueryAggregator/QueryAggregatorInterface.php rename to guzzle/guzzle/src/Guzzle/Http/QueryAggregator/QueryAggregatorInterface.php diff --git a/guzzle/http/Guzzle/Http/QueryString.php b/guzzle/guzzle/src/Guzzle/Http/QueryString.php similarity index 100% rename from guzzle/http/Guzzle/Http/QueryString.php rename to guzzle/guzzle/src/Guzzle/Http/QueryString.php diff --git a/guzzle/http/Guzzle/Http/ReadLimitEntityBody.php b/guzzle/guzzle/src/Guzzle/Http/ReadLimitEntityBody.php similarity index 100% rename from guzzle/http/Guzzle/Http/ReadLimitEntityBody.php rename to guzzle/guzzle/src/Guzzle/Http/ReadLimitEntityBody.php diff --git a/guzzle/http/Guzzle/Http/RedirectPlugin.php b/guzzle/guzzle/src/Guzzle/Http/RedirectPlugin.php similarity index 100% rename from guzzle/http/Guzzle/Http/RedirectPlugin.php rename to guzzle/guzzle/src/Guzzle/Http/RedirectPlugin.php diff --git a/guzzle/http/Guzzle/Http/Resources/cacert.pem b/guzzle/guzzle/src/Guzzle/Http/Resources/cacert.pem similarity index 100% rename from guzzle/http/Guzzle/Http/Resources/cacert.pem rename to guzzle/guzzle/src/Guzzle/Http/Resources/cacert.pem diff --git a/guzzle/http/Guzzle/Http/Resources/cacert.pem.md5 b/guzzle/guzzle/src/Guzzle/Http/Resources/cacert.pem.md5 similarity index 100% rename from guzzle/http/Guzzle/Http/Resources/cacert.pem.md5 rename to guzzle/guzzle/src/Guzzle/Http/Resources/cacert.pem.md5 diff --git a/guzzle/http/Guzzle/Http/StaticClient.php b/guzzle/guzzle/src/Guzzle/Http/StaticClient.php similarity index 100% rename from guzzle/http/Guzzle/Http/StaticClient.php rename to guzzle/guzzle/src/Guzzle/Http/StaticClient.php diff --git a/guzzle/http/Guzzle/Http/Url.php b/guzzle/guzzle/src/Guzzle/Http/Url.php similarity index 100% rename from guzzle/http/Guzzle/Http/Url.php rename to guzzle/guzzle/src/Guzzle/Http/Url.php diff --git a/guzzle/http/Guzzle/Http/composer.json b/guzzle/guzzle/src/Guzzle/Http/composer.json similarity index 100% rename from guzzle/http/Guzzle/Http/composer.json rename to guzzle/guzzle/src/Guzzle/Http/composer.json diff --git a/guzzle/guzzle/src/Guzzle/Inflection/Inflector.php b/guzzle/guzzle/src/Guzzle/Inflection/Inflector.php new file mode 100644 index 000000000..c6997734c --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Inflection/Inflector.php @@ -0,0 +1,38 @@ + array(), + 'camel' => array() + ); + + /** @var int Max entries per cache */ + protected $maxCacheSize; + + /** @var InflectorInterface Decorated inflector */ + protected $decoratedInflector; + + /** + * @param InflectorInterface $inflector Inflector being decorated + * @param int $maxCacheSize Maximum number of cached items to hold per cache + */ + public function __construct(InflectorInterface $inflector, $maxCacheSize = 500) + { + $this->decoratedInflector = $inflector; + $this->maxCacheSize = $maxCacheSize; + } + + public function snake($word) + { + if (!isset($this->cache['snake'][$word])) { + $this->pruneCache('snake'); + $this->cache['snake'][$word] = $this->decoratedInflector->snake($word); + } + + return $this->cache['snake'][$word]; + } + + /** + * Converts strings from snake_case to upper CamelCase + * + * @param string $word Value to convert into upper CamelCase + * + * @return string + */ + public function camel($word) + { + if (!isset($this->cache['camel'][$word])) { + $this->pruneCache('camel'); + $this->cache['camel'][$word] = $this->decoratedInflector->camel($word); + } + + return $this->cache['camel'][$word]; + } + + /** + * Prune one of the named caches by removing 20% of the cache if it is full + * + * @param string $cache Type of cache to prune + */ + protected function pruneCache($cache) + { + if (count($this->cache[$cache]) == $this->maxCacheSize) { + $this->cache[$cache] = array_slice($this->cache[$cache], $this->maxCacheSize * 0.2); + } + } +} diff --git a/guzzle/guzzle/src/Guzzle/Inflection/PreComputedInflector.php b/guzzle/guzzle/src/Guzzle/Inflection/PreComputedInflector.php new file mode 100644 index 000000000..db37e4fe4 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Inflection/PreComputedInflector.php @@ -0,0 +1,59 @@ + array(), + 'camel' => array() + ); + + /** @var InflectorInterface Decorated inflector */ + protected $decoratedInflector; + + /** + * @param InflectorInterface $inflector Inflector being decorated + * @param array $snake Hash of pre-computed camel to snake + * @param array $camel Hash of pre-computed snake to camel + * @param bool $mirror Mirror snake and camel reflections + */ + public function __construct(InflectorInterface $inflector, array $snake = array(), array $camel = array(), $mirror = false) + { + if ($mirror) { + $camel = array_merge(array_flip($snake), $camel); + $snake = array_merge(array_flip($camel), $snake); + } + + $this->decoratedInflector = $inflector; + $this->mapping = array( + 'snake' => $snake, + 'camel' => $camel + ); + } + + public function snake($word) + { + return isset($this->mapping['snake'][$word]) + ? $this->mapping['snake'][$word] + : $this->decoratedInflector->snake($word); + } + + /** + * Converts strings from snake_case to upper CamelCase + * + * @param string $word Value to convert into upper CamelCase + * + * @return string + */ + public function camel($word) + { + return isset($this->mapping['camel'][$word]) + ? $this->mapping['camel'][$word] + : $this->decoratedInflector->camel($word); + } +} diff --git a/guzzle/guzzle/src/Guzzle/Inflection/composer.json b/guzzle/guzzle/src/Guzzle/Inflection/composer.json new file mode 100644 index 000000000..93f9e7b72 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Inflection/composer.json @@ -0,0 +1,26 @@ +{ + "name": "guzzle/inflection", + "description": "Guzzle inflection component", + "homepage": "http://guzzlephp.org/", + "keywords": ["inflection", "guzzle"], + "license": "MIT", + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "require": { + "php": ">=5.3.2" + }, + "autoload": { + "psr-0": { "Guzzle\\Inflection": "" } + }, + "target-dir": "Guzzle/Inflection", + "extra": { + "branch-alias": { + "dev-master": "3.7-dev" + } + } +} diff --git a/guzzle/guzzle/src/Guzzle/Iterator/AppendIterator.php b/guzzle/guzzle/src/Guzzle/Iterator/AppendIterator.php new file mode 100644 index 000000000..1b6bd7e53 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Iterator/AppendIterator.php @@ -0,0 +1,19 @@ +getArrayIterator()->append($iterator); + } +} diff --git a/guzzle/guzzle/src/Guzzle/Iterator/ChunkedIterator.php b/guzzle/guzzle/src/Guzzle/Iterator/ChunkedIterator.php new file mode 100644 index 000000000..d76cdd439 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Iterator/ChunkedIterator.php @@ -0,0 +1,56 @@ +chunkSize = $chunkSize; + } + + public function rewind() + { + parent::rewind(); + $this->next(); + } + + public function next() + { + $this->chunk = array(); + for ($i = 0; $i < $this->chunkSize && parent::valid(); $i++) { + $this->chunk[] = parent::current(); + parent::next(); + } + } + + public function current() + { + return $this->chunk; + } + + public function valid() + { + return (bool) $this->chunk; + } +} diff --git a/guzzle/guzzle/src/Guzzle/Iterator/FilterIterator.php b/guzzle/guzzle/src/Guzzle/Iterator/FilterIterator.php new file mode 100644 index 000000000..b103367b6 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Iterator/FilterIterator.php @@ -0,0 +1,36 @@ +callback = $callback; + } + + public function accept() + { + return call_user_func($this->callback, $this->current()); + } +} diff --git a/guzzle/guzzle/src/Guzzle/Iterator/MapIterator.php b/guzzle/guzzle/src/Guzzle/Iterator/MapIterator.php new file mode 100644 index 000000000..7e586bda6 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Iterator/MapIterator.php @@ -0,0 +1,34 @@ +callback = $callback; + } + + public function current() + { + return call_user_func($this->callback, parent::current()); + } +} diff --git a/guzzle/guzzle/src/Guzzle/Iterator/MethodProxyIterator.php b/guzzle/guzzle/src/Guzzle/Iterator/MethodProxyIterator.php new file mode 100644 index 000000000..de4ab0360 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Iterator/MethodProxyIterator.php @@ -0,0 +1,27 @@ +getInnerIterator(); + while ($i instanceof \OuterIterator) { + $i = $i->getInnerIterator(); + } + + return call_user_func_array(array($i, $name), $args); + } +} diff --git a/guzzle/guzzle/src/Guzzle/Iterator/README.md b/guzzle/guzzle/src/Guzzle/Iterator/README.md new file mode 100644 index 000000000..8bb7e08e2 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Iterator/README.md @@ -0,0 +1,25 @@ +Guzzle Iterator +=============== + +Provides useful Iterators and Iterator decorators + +- ChunkedIterator: Pulls out chunks from an inner iterator and yields the chunks as arrays +- FilterIterator: Used when PHP 5.4's CallbackFilterIterator is not available +- MapIterator: Maps values before yielding +- MethodProxyIterator: Proxies missing method calls to the innermost iterator + +### Installing via Composer + +```bash +# Install Composer +curl -sS https://getcomposer.org/installer | php + +# Add Guzzle as a dependency +php composer.phar require guzzle/iterator:~3.0 +``` + +After installing, you need to require Composer's autoloader: + +```php +require 'vendor/autoload.php'; +``` diff --git a/guzzle/guzzle/src/Guzzle/Iterator/composer.json b/guzzle/guzzle/src/Guzzle/Iterator/composer.json new file mode 100644 index 000000000..17e800526 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Iterator/composer.json @@ -0,0 +1,27 @@ +{ + "name": "guzzle/iterator", + "description": "Provides helpful iterators and iterator decorators", + "keywords": ["iterator", "guzzle"], + "homepage": "http://guzzlephp.org/", + "license": "MIT", + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "require": { + "php": ">=5.3.2", + "guzzle/common": ">=2.8.0" + }, + "autoload": { + "psr-0": { "Guzzle\\Iterator": "/" } + }, + "target-dir": "Guzzle/Log", + "extra": { + "branch-alias": { + "dev-master": "3.7-dev" + } + } +} diff --git a/guzzle/guzzle/src/Guzzle/Log/AbstractLogAdapter.php b/guzzle/guzzle/src/Guzzle/Log/AbstractLogAdapter.php new file mode 100644 index 000000000..7f6271bcb --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Log/AbstractLogAdapter.php @@ -0,0 +1,16 @@ +log; + } +} diff --git a/guzzle/guzzle/src/Guzzle/Log/ArrayLogAdapter.php b/guzzle/guzzle/src/Guzzle/Log/ArrayLogAdapter.php new file mode 100644 index 000000000..a70fc8d42 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Log/ArrayLogAdapter.php @@ -0,0 +1,34 @@ +logs[] = array('message' => $message, 'priority' => $priority, 'extras' => $extras); + } + + /** + * Get logged entries + * + * @return array + */ + public function getLogs() + { + return $this->logs; + } + + /** + * Clears logged entries + */ + public function clearLogs() + { + $this->logs = array(); + } +} diff --git a/guzzle/guzzle/src/Guzzle/Log/ClosureLogAdapter.php b/guzzle/guzzle/src/Guzzle/Log/ClosureLogAdapter.php new file mode 100644 index 000000000..d4bb73f21 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Log/ClosureLogAdapter.php @@ -0,0 +1,23 @@ +log = $logObject; + } + + public function log($message, $priority = LOG_INFO, $extras = array()) + { + call_user_func($this->log, $message, $priority, $extras); + } +} diff --git a/guzzle/guzzle/src/Guzzle/Log/LogAdapterInterface.php b/guzzle/guzzle/src/Guzzle/Log/LogAdapterInterface.php new file mode 100644 index 000000000..d7ac4ea7c --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Log/LogAdapterInterface.php @@ -0,0 +1,18 @@ +>>>>>>>\n{request}\n<<<<<<<<\n{response}\n--------\n{curl_stderr}"; + const SHORT_FORMAT = '[{ts}] "{method} {resource} {protocol}/{version}" {code}'; + + /** + * @var string Template used to format log messages + */ + protected $template; + + /** + * @param string $template Log message template + */ + public function __construct($template = self::DEFAULT_FORMAT) + { + $this->template = $template ?: self::DEFAULT_FORMAT; + } + + /** + * Set the template to use for logging + * + * @param string $template Log message template + * + * @return self + */ + public function setTemplate($template) + { + $this->template = $template; + + return $this; + } + + /** + * Returns a formatted message + * + * @param RequestInterface $request Request that was sent + * @param Response $response Response that was received + * @param CurlHandle $handle Curl handle associated with the message + * @param array $customData Associative array of custom template data + * + * @return string + */ + public function format( + RequestInterface $request, + Response $response = null, + CurlHandle $handle = null, + array $customData = array() + ) { + $cache = $customData; + + return preg_replace_callback( + '/{\s*([A-Za-z_\-\.0-9]+)\s*}/', + function (array $matches) use ($request, $response, $handle, &$cache) { + + if (array_key_exists($matches[1], $cache)) { + return $cache[$matches[1]]; + } + + $result = ''; + switch ($matches[1]) { + case 'request': + $result = (string) $request; + break; + case 'response': + $result = (string) $response; + break; + case 'req_body': + $result = $request instanceof EntityEnclosingRequestInterface + ? (string) $request->getBody() : ''; + break; + case 'res_body': + $result = $response ? $response->getBody(true) : ''; + break; + case 'ts': + $result = gmdate('c'); + break; + case 'method': + $result = $request->getMethod(); + break; + case 'url': + $result = (string) $request->getUrl(); + break; + case 'resource': + $result = $request->getResource(); + break; + case 'protocol': + $result = 'HTTP'; + break; + case 'version': + $result = $request->getProtocolVersion(); + break; + case 'host': + $result = $request->getHost(); + break; + case 'hostname': + $result = gethostname(); + break; + case 'port': + $result = $request->getPort(); + break; + case 'code': + $result = $response ? $response->getStatusCode() : ''; + break; + case 'phrase': + $result = $response ? $response->getReasonPhrase() : ''; + break; + case 'connect_time': + $result = $handle && $handle->getInfo(CURLINFO_CONNECT_TIME) + ? $handle->getInfo(CURLINFO_CONNECT_TIME) + : ($response ? $response->getInfo('connect_time') : ''); + break; + case 'total_time': + $result = $handle && $handle->getInfo(CURLINFO_TOTAL_TIME) + ? $handle->getInfo(CURLINFO_TOTAL_TIME) + : ($response ? $response->getInfo('total_time') : ''); + break; + case 'curl_error': + $result = $handle ? $handle->getError() : ''; + break; + case 'curl_code': + $result = $handle ? $handle->getErrorNo() : ''; + break; + case 'curl_stderr': + $result = $handle ? $handle->getStderr() : ''; + break; + default: + if (strpos($matches[1], 'req_header_') === 0) { + $result = $request->getHeader(substr($matches[1], 11)); + } elseif ($response && strpos($matches[1], 'res_header_') === 0) { + $result = $response->getHeader(substr($matches[1], 11)); + } + } + + $cache[$matches[1]] = $result; + return $result; + }, + $this->template + ); + } +} diff --git a/guzzle/guzzle/src/Guzzle/Log/MonologLogAdapter.php b/guzzle/guzzle/src/Guzzle/Log/MonologLogAdapter.php new file mode 100644 index 000000000..8c74a45dc --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Log/MonologLogAdapter.php @@ -0,0 +1,34 @@ + Logger::DEBUG, + LOG_INFO => Logger::INFO, + LOG_WARNING => Logger::WARNING, + LOG_ERR => Logger::ERROR, + LOG_CRIT => Logger::CRITICAL, + LOG_ALERT => Logger::ALERT + ); + + public function __construct(Logger $logObject) + { + $this->log = $logObject; + } + + public function log($message, $priority = LOG_INFO, $extras = array()) + { + $this->log->addRecord(self::$mapping[$priority], $message); + } +} diff --git a/guzzle/guzzle/src/Guzzle/Log/PsrLogAdapter.php b/guzzle/guzzle/src/Guzzle/Log/PsrLogAdapter.php new file mode 100644 index 000000000..38a2b600d --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Log/PsrLogAdapter.php @@ -0,0 +1,36 @@ + LogLevel::DEBUG, + LOG_INFO => LogLevel::INFO, + LOG_WARNING => LogLevel::WARNING, + LOG_ERR => LogLevel::ERROR, + LOG_CRIT => LogLevel::CRITICAL, + LOG_ALERT => LogLevel::ALERT + ); + + public function __construct(LoggerInterface $logObject) + { + $this->log = $logObject; + } + + public function log($message, $priority = LOG_INFO, $extras = array()) + { + $this->log->log(self::$mapping[$priority], $message, $extras); + } +} diff --git a/guzzle/guzzle/src/Guzzle/Log/Zf1LogAdapter.php b/guzzle/guzzle/src/Guzzle/Log/Zf1LogAdapter.php new file mode 100644 index 000000000..0ea8e3b1d --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Log/Zf1LogAdapter.php @@ -0,0 +1,24 @@ +log = $logObject; + Version::warn(__CLASS__ . ' is deprecated'); + } + + public function log($message, $priority = LOG_INFO, $extras = array()) + { + $this->log->log($message, $priority, $extras); + } +} diff --git a/guzzle/guzzle/src/Guzzle/Log/Zf2LogAdapter.php b/guzzle/guzzle/src/Guzzle/Log/Zf2LogAdapter.php new file mode 100644 index 000000000..863f6a1c4 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Log/Zf2LogAdapter.php @@ -0,0 +1,21 @@ +log = $logObject; + } + + public function log($message, $priority = LOG_INFO, $extras = array()) + { + $this->log->log($priority, $message, $extras); + } +} diff --git a/guzzle/guzzle/src/Guzzle/Log/composer.json b/guzzle/guzzle/src/Guzzle/Log/composer.json new file mode 100644 index 000000000..a8213e8b4 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Log/composer.json @@ -0,0 +1,29 @@ +{ + "name": "guzzle/log", + "description": "Guzzle log adapter component", + "homepage": "http://guzzlephp.org/", + "keywords": ["log", "adapter", "guzzle"], + "license": "MIT", + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "require": { + "php": ">=5.3.2" + }, + "autoload": { + "psr-0": { "Guzzle\\Log": "" } + }, + "suggest": { + "guzzle/http": "self.version" + }, + "target-dir": "Guzzle/Log", + "extra": { + "branch-alias": { + "dev-master": "3.7-dev" + } + } +} diff --git a/guzzle/parser/Guzzle/Parser/Cookie/CookieParser.php b/guzzle/guzzle/src/Guzzle/Parser/Cookie/CookieParser.php similarity index 100% rename from guzzle/parser/Guzzle/Parser/Cookie/CookieParser.php rename to guzzle/guzzle/src/Guzzle/Parser/Cookie/CookieParser.php diff --git a/guzzle/parser/Guzzle/Parser/Cookie/CookieParserInterface.php b/guzzle/guzzle/src/Guzzle/Parser/Cookie/CookieParserInterface.php similarity index 100% rename from guzzle/parser/Guzzle/Parser/Cookie/CookieParserInterface.php rename to guzzle/guzzle/src/Guzzle/Parser/Cookie/CookieParserInterface.php diff --git a/guzzle/parser/Guzzle/Parser/Message/AbstractMessageParser.php b/guzzle/guzzle/src/Guzzle/Parser/Message/AbstractMessageParser.php similarity index 100% rename from guzzle/parser/Guzzle/Parser/Message/AbstractMessageParser.php rename to guzzle/guzzle/src/Guzzle/Parser/Message/AbstractMessageParser.php diff --git a/guzzle/parser/Guzzle/Parser/Message/MessageParser.php b/guzzle/guzzle/src/Guzzle/Parser/Message/MessageParser.php similarity index 100% rename from guzzle/parser/Guzzle/Parser/Message/MessageParser.php rename to guzzle/guzzle/src/Guzzle/Parser/Message/MessageParser.php diff --git a/guzzle/parser/Guzzle/Parser/Message/MessageParserInterface.php b/guzzle/guzzle/src/Guzzle/Parser/Message/MessageParserInterface.php similarity index 100% rename from guzzle/parser/Guzzle/Parser/Message/MessageParserInterface.php rename to guzzle/guzzle/src/Guzzle/Parser/Message/MessageParserInterface.php diff --git a/guzzle/parser/Guzzle/Parser/Message/PeclHttpMessageParser.php b/guzzle/guzzle/src/Guzzle/Parser/Message/PeclHttpMessageParser.php similarity index 100% rename from guzzle/parser/Guzzle/Parser/Message/PeclHttpMessageParser.php rename to guzzle/guzzle/src/Guzzle/Parser/Message/PeclHttpMessageParser.php diff --git a/guzzle/parser/Guzzle/Parser/ParserRegistry.php b/guzzle/guzzle/src/Guzzle/Parser/ParserRegistry.php similarity index 100% rename from guzzle/parser/Guzzle/Parser/ParserRegistry.php rename to guzzle/guzzle/src/Guzzle/Parser/ParserRegistry.php diff --git a/guzzle/parser/Guzzle/Parser/UriTemplate/PeclUriTemplate.php b/guzzle/guzzle/src/Guzzle/Parser/UriTemplate/PeclUriTemplate.php similarity index 100% rename from guzzle/parser/Guzzle/Parser/UriTemplate/PeclUriTemplate.php rename to guzzle/guzzle/src/Guzzle/Parser/UriTemplate/PeclUriTemplate.php diff --git a/guzzle/parser/Guzzle/Parser/UriTemplate/UriTemplate.php b/guzzle/guzzle/src/Guzzle/Parser/UriTemplate/UriTemplate.php similarity index 100% rename from guzzle/parser/Guzzle/Parser/UriTemplate/UriTemplate.php rename to guzzle/guzzle/src/Guzzle/Parser/UriTemplate/UriTemplate.php diff --git a/guzzle/parser/Guzzle/Parser/UriTemplate/UriTemplateInterface.php b/guzzle/guzzle/src/Guzzle/Parser/UriTemplate/UriTemplateInterface.php similarity index 100% rename from guzzle/parser/Guzzle/Parser/UriTemplate/UriTemplateInterface.php rename to guzzle/guzzle/src/Guzzle/Parser/UriTemplate/UriTemplateInterface.php diff --git a/guzzle/parser/Guzzle/Parser/Url/UrlParser.php b/guzzle/guzzle/src/Guzzle/Parser/Url/UrlParser.php similarity index 100% rename from guzzle/parser/Guzzle/Parser/Url/UrlParser.php rename to guzzle/guzzle/src/Guzzle/Parser/Url/UrlParser.php diff --git a/guzzle/parser/Guzzle/Parser/Url/UrlParserInterface.php b/guzzle/guzzle/src/Guzzle/Parser/Url/UrlParserInterface.php similarity index 100% rename from guzzle/parser/Guzzle/Parser/Url/UrlParserInterface.php rename to guzzle/guzzle/src/Guzzle/Parser/Url/UrlParserInterface.php diff --git a/guzzle/parser/Guzzle/Parser/composer.json b/guzzle/guzzle/src/Guzzle/Parser/composer.json similarity index 100% rename from guzzle/parser/Guzzle/Parser/composer.json rename to guzzle/guzzle/src/Guzzle/Parser/composer.json diff --git a/guzzle/guzzle/src/Guzzle/Plugin/Async/AsyncPlugin.php b/guzzle/guzzle/src/Guzzle/Plugin/Async/AsyncPlugin.php new file mode 100644 index 000000000..ae5941873 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Plugin/Async/AsyncPlugin.php @@ -0,0 +1,84 @@ + 'onBeforeSend', + 'request.exception' => 'onRequestTimeout', + 'request.sent' => 'onRequestSent', + 'curl.callback.progress' => 'onCurlProgress' + ); + } + + /** + * Event used to ensure that progress callback are emitted from the curl handle's request mediator. + * + * @param Event $event + */ + public function onBeforeSend(Event $event) + { + // Ensure that progress callbacks are dispatched + $event['request']->getCurlOptions()->set('progress', true); + } + + /** + * Event emitted when a curl progress function is called. When the amount of data uploaded == the amount of data to + * upload OR any bytes have been downloaded, then time the request out after 1ms because we're done with + * transmitting the request, and tell curl not download a body. + * + * @param Event $event + */ + public function onCurlProgress(Event $event) + { + if ($event['handle'] && + ($event['downloaded'] || (isset($event['uploaded']) && $event['upload_size'] === $event['uploaded'])) + ) { + // Timeout after 1ms + curl_setopt($event['handle'], CURLOPT_TIMEOUT_MS, 1); + // Even if the response is quick, tell curl not to download the body. + // - Note that we can only perform this shortcut if the request transmitted a body so as to ensure that the + // request method is not converted to a HEAD request before the request was sent via curl. + if ($event['uploaded']) { + curl_setopt($event['handle'], CURLOPT_NOBODY, true); + } + } + } + + /** + * Event emitted when a curl exception occurs. Ignore the exception and set a mock response. + * + * @param Event $event + */ + public function onRequestTimeout(Event $event) + { + if ($event['exception'] instanceof CurlException) { + $event['request']->setResponse(new Response(200, array( + 'X-Guzzle-Async' => 'Did not wait for the response' + ))); + } + } + + /** + * Event emitted when a request completes because it took less than 1ms. Add an X-Guzzle-Async header to notify the + * caller that there is no body in the message. + * + * @param Event $event + */ + public function onRequestSent(Event $event) + { + // Let the caller know this was meant to be async + $event['request']->getResponse()->setHeader('X-Guzzle-Async', 'Did not wait for the response'); + } +} diff --git a/guzzle/guzzle/src/Guzzle/Plugin/Async/composer.json b/guzzle/guzzle/src/Guzzle/Plugin/Async/composer.json new file mode 100644 index 000000000..dc3fc5bf8 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Plugin/Async/composer.json @@ -0,0 +1,27 @@ +{ + "name": "guzzle/plugin-async", + "description": "Guzzle async request plugin", + "homepage": "http://guzzlephp.org/", + "keywords": ["plugin", "guzzle"], + "license": "MIT", + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "require": { + "php": ">=5.3.2", + "guzzle/http": "self.version" + }, + "autoload": { + "psr-0": { "Guzzle\\Plugin\\Async": "" } + }, + "target-dir": "Guzzle/Plugin/Async", + "extra": { + "branch-alias": { + "dev-master": "3.7-dev" + } + } +} diff --git a/guzzle/guzzle/src/Guzzle/Plugin/Backoff/AbstractBackoffStrategy.php b/guzzle/guzzle/src/Guzzle/Plugin/Backoff/AbstractBackoffStrategy.php new file mode 100644 index 000000000..0a8598345 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Plugin/Backoff/AbstractBackoffStrategy.php @@ -0,0 +1,91 @@ +next = $next; + } + + /** + * Get the next backoff strategy in the chain + * + * @return AbstractBackoffStrategy|null + */ + public function getNext() + { + return $this->next; + } + + public function getBackoffPeriod( + $retries, + RequestInterface $request, + Response $response = null, + HttpException $e = null + ) { + $delay = $this->getDelay($retries, $request, $response, $e); + if ($delay === false) { + // The strategy knows that this must not be retried + return false; + } elseif ($delay === null) { + // If the strategy is deferring a decision and the next strategy will not make a decision then return false + return !$this->next || !$this->next->makesDecision() + ? false + : $this->next->getBackoffPeriod($retries, $request, $response, $e); + } elseif ($delay === true) { + // if the strategy knows that it must retry but is deferring to the next to determine the delay + if (!$this->next) { + return 0; + } else { + $next = $this->next; + while ($next->makesDecision() && $next->getNext()) { + $next = $next->getNext(); + } + return !$next->makesDecision() ? $next->getBackoffPeriod($retries, $request, $response, $e) : 0; + } + } else { + return $delay; + } + } + + /** + * Check if the strategy does filtering and makes decisions on whether or not to retry. + * + * Strategies that return false will never retry if all of the previous strategies in a chain defer on a backoff + * decision. + * + * @return bool + */ + abstract public function makesDecision(); + + /** + * Implement the concrete strategy + * + * @param int $retries Number of retries of the request + * @param RequestInterface $request Request that was sent + * @param Response $response Response that was received. Note that there may not be a response + * @param HttpException $e Exception that was encountered if any + * + * @return bool|int|null Returns false to not retry or the number of seconds to delay between retries. Return true + * or null to defer to the next strategy if available, and if not, return 0. + */ + abstract protected function getDelay( + $retries, + RequestInterface $request, + Response $response = null, + HttpException $e = null + ); +} diff --git a/guzzle/guzzle/src/Guzzle/Plugin/Backoff/AbstractErrorCodeBackoffStrategy.php b/guzzle/guzzle/src/Guzzle/Plugin/Backoff/AbstractErrorCodeBackoffStrategy.php new file mode 100644 index 000000000..6ebee6c1a --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Plugin/Backoff/AbstractErrorCodeBackoffStrategy.php @@ -0,0 +1,40 @@ +errorCodes = array_fill_keys($codes ?: static::$defaultErrorCodes, 1); + $this->next = $next; + } + + /** + * Get the default failure codes to retry + * + * @return array + */ + public static function getDefaultFailureCodes() + { + return static::$defaultErrorCodes; + } + + public function makesDecision() + { + return true; + } +} diff --git a/guzzle/guzzle/src/Guzzle/Plugin/Backoff/BackoffLogger.php b/guzzle/guzzle/src/Guzzle/Plugin/Backoff/BackoffLogger.php new file mode 100644 index 000000000..ec54c289e --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Plugin/Backoff/BackoffLogger.php @@ -0,0 +1,76 @@ +logger = $logger; + $this->formatter = $formatter ?: new MessageFormatter(self::DEFAULT_FORMAT); + } + + public static function getSubscribedEvents() + { + return array(BackoffPlugin::RETRY_EVENT => 'onRequestRetry'); + } + + /** + * Set the template to use for logging + * + * @param string $template Log message template + * + * @return self + */ + public function setTemplate($template) + { + $this->formatter->setTemplate($template); + + return $this; + } + + /** + * Called when a request is being retried + * + * @param Event $event Event emitted + */ + public function onRequestRetry(Event $event) + { + $this->logger->log($this->formatter->format( + $event['request'], + $event['response'], + $event['handle'], + array( + 'retries' => $event['retries'], + 'delay' => $event['delay'] + ) + )); + } +} diff --git a/guzzle/guzzle/src/Guzzle/Plugin/Backoff/BackoffPlugin.php b/guzzle/guzzle/src/Guzzle/Plugin/Backoff/BackoffPlugin.php new file mode 100644 index 000000000..99ace0538 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Plugin/Backoff/BackoffPlugin.php @@ -0,0 +1,126 @@ +strategy = $strategy; + } + + /** + * Retrieve a basic truncated exponential backoff plugin that will retry HTTP errors and cURL errors + * + * @param int $maxRetries Maximum number of retries + * @param array $httpCodes HTTP response codes to retry + * @param array $curlCodes cURL error codes to retry + * + * @return self + */ + public static function getExponentialBackoff( + $maxRetries = 3, + array $httpCodes = null, + array $curlCodes = null + ) { + return new self(new TruncatedBackoffStrategy($maxRetries, + new HttpBackoffStrategy($httpCodes, + new CurlBackoffStrategy($curlCodes, + new ExponentialBackoffStrategy() + ) + ) + )); + } + + public static function getAllEvents() + { + return array(self::RETRY_EVENT); + } + + public static function getSubscribedEvents() + { + return array( + 'request.sent' => 'onRequestSent', + 'request.exception' => 'onRequestSent', + CurlMultiInterface::POLLING_REQUEST => 'onRequestPoll' + ); + } + + /** + * Called when a request has been sent and isn't finished processing + * + * @param Event $event + */ + public function onRequestSent(Event $event) + { + $request = $event['request']; + $response = $event['response']; + $exception = $event['exception']; + + $params = $request->getParams(); + $retries = (int) $params->get(self::RETRY_PARAM); + $delay = $this->strategy->getBackoffPeriod($retries, $request, $response, $exception); + + if ($delay !== false) { + // Calculate how long to wait until the request should be retried + $params->set(self::RETRY_PARAM, ++$retries) + ->set(self::DELAY_PARAM, microtime(true) + $delay); + // Send the request again + $request->setState(RequestInterface::STATE_TRANSFER); + $this->dispatch(self::RETRY_EVENT, array( + 'request' => $request, + 'response' => $response, + 'handle' => ($exception && $exception instanceof CurlException) ? $exception->getCurlHandle() : null, + 'retries' => $retries, + 'delay' => $delay + )); + } + } + + /** + * Called when a request is polling in the curl multi object + * + * @param Event $event + */ + public function onRequestPoll(Event $event) + { + $request = $event['request']; + $delay = $request->getParams()->get(self::DELAY_PARAM); + + // If the duration of the delay has passed, retry the request using the pool + if (null !== $delay && microtime(true) >= $delay) { + // Remove the request from the pool and then add it back again. This is required for cURL to know that we + // want to retry sending the easy handle. + $request->getParams()->remove(self::DELAY_PARAM); + // Rewind the request body if possible + if ($request instanceof EntityEnclosingRequestInterface && $request->getBody()) { + $request->getBody()->seek(0); + } + $multi = $event['curl_multi']; + $multi->remove($request); + $multi->add($request); + } + } +} diff --git a/guzzle/guzzle/src/Guzzle/Plugin/Backoff/BackoffStrategyInterface.php b/guzzle/guzzle/src/Guzzle/Plugin/Backoff/BackoffStrategyInterface.php new file mode 100644 index 000000000..4e590dbe0 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Plugin/Backoff/BackoffStrategyInterface.php @@ -0,0 +1,30 @@ +callback = $callback; + $this->decision = (bool) $decision; + $this->next = $next; + } + + public function makesDecision() + { + return $this->decision; + } + + protected function getDelay($retries, RequestInterface $request, Response $response = null, HttpException $e = null) + { + return call_user_func($this->callback, $retries, $request, $response, $e); + } +} diff --git a/guzzle/guzzle/src/Guzzle/Plugin/Backoff/ConstantBackoffStrategy.php b/guzzle/guzzle/src/Guzzle/Plugin/Backoff/ConstantBackoffStrategy.php new file mode 100644 index 000000000..061d2a407 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Plugin/Backoff/ConstantBackoffStrategy.php @@ -0,0 +1,34 @@ +delay = $delay; + } + + public function makesDecision() + { + return false; + } + + protected function getDelay($retries, RequestInterface $request, Response $response = null, HttpException $e = null) + { + return $this->delay; + } +} diff --git a/guzzle/guzzle/src/Guzzle/Plugin/Backoff/CurlBackoffStrategy.php b/guzzle/guzzle/src/Guzzle/Plugin/Backoff/CurlBackoffStrategy.php new file mode 100644 index 000000000..a584ed4a2 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Plugin/Backoff/CurlBackoffStrategy.php @@ -0,0 +1,28 @@ +errorCodes[$e->getErrorNo()]) ? true : null; + } + } +} diff --git a/guzzle/guzzle/src/Guzzle/Plugin/Backoff/ExponentialBackoffStrategy.php b/guzzle/guzzle/src/Guzzle/Plugin/Backoff/ExponentialBackoffStrategy.php new file mode 100644 index 000000000..fb2912d50 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Plugin/Backoff/ExponentialBackoffStrategy.php @@ -0,0 +1,25 @@ +isSuccessful()) { + return false; + } else { + return isset($this->errorCodes[$response->getStatusCode()]) ? true : null; + } + } + } +} diff --git a/guzzle/guzzle/src/Guzzle/Plugin/Backoff/LinearBackoffStrategy.php b/guzzle/guzzle/src/Guzzle/Plugin/Backoff/LinearBackoffStrategy.php new file mode 100644 index 000000000..b35e8a490 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Plugin/Backoff/LinearBackoffStrategy.php @@ -0,0 +1,36 @@ +step = $step; + } + + public function makesDecision() + { + return false; + } + + protected function getDelay($retries, RequestInterface $request, Response $response = null, HttpException $e = null) + { + return $retries * $this->step; + } +} diff --git a/guzzle/guzzle/src/Guzzle/Plugin/Backoff/ReasonPhraseBackoffStrategy.php b/guzzle/guzzle/src/Guzzle/Plugin/Backoff/ReasonPhraseBackoffStrategy.php new file mode 100644 index 000000000..4fd73fedf --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Plugin/Backoff/ReasonPhraseBackoffStrategy.php @@ -0,0 +1,25 @@ +errorCodes[$response->getReasonPhrase()]) ? true : null; + } + } +} diff --git a/guzzle/guzzle/src/Guzzle/Plugin/Backoff/TruncatedBackoffStrategy.php b/guzzle/guzzle/src/Guzzle/Plugin/Backoff/TruncatedBackoffStrategy.php new file mode 100644 index 000000000..3608f3584 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Plugin/Backoff/TruncatedBackoffStrategy.php @@ -0,0 +1,36 @@ +max = $maxRetries; + $this->next = $next; + } + + public function makesDecision() + { + return true; + } + + protected function getDelay($retries, RequestInterface $request, Response $response = null, HttpException $e = null) + { + return $retries < $this->max ? null : false; + } +} diff --git a/guzzle/guzzle/src/Guzzle/Plugin/Backoff/composer.json b/guzzle/guzzle/src/Guzzle/Plugin/Backoff/composer.json new file mode 100644 index 000000000..91c122cb4 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Plugin/Backoff/composer.json @@ -0,0 +1,28 @@ +{ + "name": "guzzle/plugin-backoff", + "description": "Guzzle backoff retry plugins", + "homepage": "http://guzzlephp.org/", + "keywords": ["plugin", "guzzle"], + "license": "MIT", + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "require": { + "php": ">=5.3.2", + "guzzle/http": "self.version", + "guzzle/log": "self.version" + }, + "autoload": { + "psr-0": { "Guzzle\\Plugin\\Backoff": "" } + }, + "target-dir": "Guzzle/Plugin/Backoff", + "extra": { + "branch-alias": { + "dev-master": "3.7-dev" + } + } +} diff --git a/guzzle/guzzle/src/Guzzle/Plugin/Cache/CacheKeyProviderInterface.php b/guzzle/guzzle/src/Guzzle/Plugin/Cache/CacheKeyProviderInterface.php new file mode 100644 index 000000000..7790f8844 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Plugin/Cache/CacheKeyProviderInterface.php @@ -0,0 +1,11 @@ + new DefaultCacheStorage($options)); + } elseif ($options instanceof CacheStorageInterface) { + $options = array('storage' => $options); + } elseif ($options) { + $options = array('storage' => new DefaultCacheStorage(CacheAdapterFactory::fromCache($options))); + } elseif (!class_exists('Doctrine\Common\Cache\ArrayCache')) { + // @codeCoverageIgnoreStart + throw new InvalidArgumentException('No cache was provided and Doctrine is not installed'); + // @codeCoverageIgnoreEnd + } + } + + $this->autoPurge = isset($options['auto_purge']) ? $options['auto_purge'] : false; + + // Add a cache storage if a cache adapter was provided + $this->storage = isset($options['storage']) + ? $options['storage'] + : new DefaultCacheStorage(new DoctrineCacheAdapter(new ArrayCache())); + + if (!isset($options['can_cache'])) { + $this->canCache = new DefaultCanCacheStrategy(); + } else { + $this->canCache = is_callable($options['can_cache']) + ? new CallbackCanCacheStrategy($options['can_cache']) + : $options['can_cache']; + } + + // Use the provided revalidation strategy or the default + $this->revalidation = isset($options['revalidation']) + ? $options['revalidation'] + : new DefaultRevalidation($this->storage, $this->canCache); + } + + public static function getSubscribedEvents() + { + return array( + 'request.before_send' => array('onRequestBeforeSend', -255), + 'request.sent' => array('onRequestSent', 255), + 'request.error' => array('onRequestError', 0), + 'request.exception' => array('onRequestException', 0), + ); + } + + /** + * Check if a response in cache will satisfy the request before sending + * + * @param Event $event + */ + public function onRequestBeforeSend(Event $event) + { + $request = $event['request']; + $request->addHeader('Via', sprintf('%s GuzzleCache/%s', $request->getProtocolVersion(), Version::VERSION)); + + if (!$this->canCache->canCacheRequest($request)) { + switch ($request->getMethod()) { + case 'PURGE': + $this->purge($request); + $request->setResponse(new Response(200, array(), 'purged')); + break; + case 'PUT': + case 'POST': + case 'DELETE': + case 'PATCH': + if ($this->autoPurge) { + $this->purge($request); + } + } + return; + } + + if ($response = $this->storage->fetch($request)) { + $params = $request->getParams(); + $params['cache.lookup'] = true; + $response->setHeader( + 'Age', + time() - strtotime($response->getDate() ? : $response->getLastModified() ?: 'now') + ); + // Validate that the response satisfies the request + if ($this->canResponseSatisfyRequest($request, $response)) { + if (!isset($params['cache.hit'])) { + $params['cache.hit'] = true; + } + $request->setResponse($response); + } + } + } + + /** + * If possible, store a response in cache after sending + * + * @param Event $event + */ + public function onRequestSent(Event $event) + { + $request = $event['request']; + $response = $event['response']; + + if ($request->getParams()->get('cache.hit') === null && + $this->canCache->canCacheRequest($request) && + $this->canCache->canCacheResponse($response) + ) { + $this->storage->cache($request, $response); + } + + $this->addResponseHeaders($request, $response); + } + + /** + * If possible, return a cache response on an error + * + * @param Event $event + */ + public function onRequestError(Event $event) + { + $request = $event['request']; + + if (!$this->canCache->canCacheRequest($request)) { + return; + } + + if ($response = $this->storage->fetch($request)) { + $response->setHeader( + 'Age', + time() - strtotime($response->getLastModified() ? : $response->getDate() ?: 'now') + ); + + if ($this->canResponseSatisfyFailedRequest($request, $response)) { + $request->getParams()->set('cache.hit', 'error'); + $this->addResponseHeaders($request, $response); + $event['response'] = $response; + $event->stopPropagation(); + } + } + } + + /** + * If possible, set a cache response on a cURL exception + * + * @param Event $event + * + * @return null + */ + public function onRequestException(Event $event) + { + if (!$event['exception'] instanceof CurlException) { + return; + } + + $request = $event['request']; + if (!$this->canCache->canCacheRequest($request)) { + return; + } + + if ($response = $this->storage->fetch($request)) { + $response->setHeader('Age', time() - strtotime($response->getDate() ? : 'now')); + if (!$this->canResponseSatisfyFailedRequest($request, $response)) { + return; + } + $request->getParams()->set('cache.hit', 'error'); + $request->setResponse($response); + $this->addResponseHeaders($request, $response); + $event->stopPropagation(); + } + } + + /** + * Check if a cache response satisfies a request's caching constraints + * + * @param RequestInterface $request Request to validate + * @param Response $response Response to validate + * + * @return bool + */ + public function canResponseSatisfyRequest(RequestInterface $request, Response $response) + { + $responseAge = $response->calculateAge(); + $reqc = $request->getHeader('Cache-Control'); + $resc = $response->getHeader('Cache-Control'); + + // Check the request's max-age header against the age of the response + if ($reqc && $reqc->hasDirective('max-age') && + $responseAge > $reqc->getDirective('max-age')) { + return false; + } + + // Check the response's max-age header + if ($response->isFresh() === false) { + $maxStale = $reqc ? $reqc->getDirective('max-stale') : null; + if (null !== $maxStale) { + if ($maxStale !== true && $response->getFreshness() < (-1 * $maxStale)) { + return false; + } + } elseif ($resc && $resc->hasDirective('max-age') + && $responseAge > $resc->getDirective('max-age') + ) { + return false; + } + } + + if ($this->revalidation->shouldRevalidate($request, $response)) { + try { + return $this->revalidation->revalidate($request, $response); + } catch (CurlException $e) { + $request->getParams()->set('cache.hit', 'error'); + return $this->canResponseSatisfyFailedRequest($request, $response); + } + } + + return true; + } + + /** + * Check if a cache response satisfies a failed request's caching constraints + * + * @param RequestInterface $request Request to validate + * @param Response $response Response to validate + * + * @return bool + */ + public function canResponseSatisfyFailedRequest(RequestInterface $request, Response $response) + { + $reqc = $request->getHeader('Cache-Control'); + $resc = $response->getHeader('Cache-Control'); + $requestStaleIfError = $reqc ? $reqc->getDirective('stale-if-error') : null; + $responseStaleIfError = $resc ? $resc->getDirective('stale-if-error') : null; + + if (!$requestStaleIfError && !$responseStaleIfError) { + return false; + } + + if (is_numeric($requestStaleIfError) && $response->getAge() - $response->getMaxAge() > $requestStaleIfError) { + return false; + } + + if (is_numeric($responseStaleIfError) && $response->getAge() - $response->getMaxAge() > $responseStaleIfError) { + return false; + } + + return true; + } + + /** + * Purge all cache entries for a given URL + * + * @param string $url URL to purge + */ + public function purge($url) + { + // BC compatibility with previous version that accepted a Request object + $url = $url instanceof RequestInterface ? $url->getUrl() : $url; + $this->storage->purge($url); + } + + /** + * Add the plugin's headers to a response + * + * @param RequestInterface $request Request + * @param Response $response Response to add headers to + */ + protected function addResponseHeaders(RequestInterface $request, Response $response) + { + $params = $request->getParams(); + $response->setHeader('Via', sprintf('%s GuzzleCache/%s', $request->getProtocolVersion(), Version::VERSION)); + + $lookup = ($params['cache.lookup'] === true ? 'HIT' : 'MISS') . ' from GuzzleCache'; + if ($header = $response->getHeader('X-Cache-Lookup')) { + // Don't add duplicates + $values = $header->toArray(); + $values[] = $lookup; + $response->setHeader('X-Cache-Lookup', array_unique($values)); + } else { + $response->setHeader('X-Cache-Lookup', $lookup); + } + + if ($params['cache.hit'] === true) { + $xcache = 'HIT from GuzzleCache'; + } elseif ($params['cache.hit'] == 'error') { + $xcache = 'HIT_ERROR from GuzzleCache'; + } else { + $xcache = 'MISS from GuzzleCache'; + } + + if ($header = $response->getHeader('X-Cache')) { + // Don't add duplicates + $values = $header->toArray(); + $values[] = $xcache; + $response->setHeader('X-Cache', array_unique($values)); + } else { + $response->setHeader('X-Cache', $xcache); + } + + if ($response->isFresh() === false) { + $response->addHeader('Warning', sprintf('110 GuzzleCache/%s "Response is stale"', Version::VERSION)); + if ($params['cache.hit'] === 'error') { + $response->addHeader('Warning', sprintf('111 GuzzleCache/%s "Revalidation failed"', Version::VERSION)); + } + } + } +} diff --git a/guzzle/guzzle/src/Guzzle/Plugin/Cache/CacheStorageInterface.php b/guzzle/guzzle/src/Guzzle/Plugin/Cache/CacheStorageInterface.php new file mode 100644 index 000000000..f3d915458 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Plugin/Cache/CacheStorageInterface.php @@ -0,0 +1,43 @@ +requestCallback = $requestCallback; + $this->responseCallback = $responseCallback; + } + + public function canCacheRequest(RequestInterface $request) + { + return $this->requestCallback + ? call_user_func($this->requestCallback, $request) + : parent::canCacheRequest($request); + } + + public function canCacheResponse(Response $response) + { + return $this->responseCallback + ? call_user_func($this->responseCallback, $response) + : parent::canCacheResponse($response); + } +} diff --git a/guzzle/guzzle/src/Guzzle/Plugin/Cache/CanCacheStrategyInterface.php b/guzzle/guzzle/src/Guzzle/Plugin/Cache/CanCacheStrategyInterface.php new file mode 100644 index 000000000..6e01a8e74 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Plugin/Cache/CanCacheStrategyInterface.php @@ -0,0 +1,30 @@ +getParams()->get(self::CACHE_KEY); + + if (!$key) { + + $cloned = clone $request; + $cloned->removeHeader('Cache-Control'); + + // Check to see how and if the key should be filtered + foreach (explode(';', $request->getParams()->get(self::CACHE_KEY_FILTER)) as $part) { + $pieces = array_map('trim', explode('=', $part)); + if (isset($pieces[1])) { + foreach (array_map('trim', explode(',', $pieces[1])) as $remove) { + if ($pieces[0] == 'header') { + $cloned->removeHeader($remove); + } elseif ($pieces[0] == 'query') { + $cloned->getQuery()->remove($remove); + } + } + } + } + + $raw = (string) $cloned; + $key = 'GZ' . md5($raw); + $request->getParams()->set(self::CACHE_KEY, $key)->set(self::CACHE_KEY_RAW, $raw); + } + + return $key; + } +} diff --git a/guzzle/guzzle/src/Guzzle/Plugin/Cache/DefaultCacheStorage.php b/guzzle/guzzle/src/Guzzle/Plugin/Cache/DefaultCacheStorage.php new file mode 100644 index 000000000..555c9b79c --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Plugin/Cache/DefaultCacheStorage.php @@ -0,0 +1,251 @@ +cache = CacheAdapterFactory::fromCache($cache); + $this->defaultTtl = $defaultTtl; + $this->keyPrefix = $keyPrefix; + } + + public function cache(RequestInterface $request, Response $response) + { + $currentTime = time(); + $ttl = $request->getParams()->get('cache.override_ttl') ?: $response->getMaxAge() ?: $this->defaultTtl; + + if ($cacheControl = $response->getHeader('Cache-Control')) { + $stale = $cacheControl->getDirective('stale-if-error'); + $ttl += $stale == true ? $ttl : $stale; + } + + // Determine which manifest key should be used + $key = $this->getCacheKey($request); + $persistedRequest = $this->persistHeaders($request); + $entries = array(); + + if ($manifest = $this->cache->fetch($key)) { + // Determine which cache entries should still be in the cache + $vary = $response->getVary(); + foreach (unserialize($manifest) as $entry) { + // Check if the entry is expired + if ($entry[4] < $currentTime) { + continue; + } + $entry[1]['vary'] = isset($entry[1]['vary']) ? $entry[1]['vary'] : ''; + if ($vary != $entry[1]['vary'] || !$this->requestsMatch($vary, $entry[0], $persistedRequest)) { + $entries[] = $entry; + } + } + } + + // Persist the response body if needed + $bodyDigest = null; + if ($response->getBody() && $response->getBody()->getContentLength() > 0) { + $bodyDigest = $this->getBodyKey($request->getUrl(), $response->getBody()); + $this->cache->save($bodyDigest, (string) $response->getBody(), $ttl); + } + + array_unshift($entries, array( + $persistedRequest, + $this->persistHeaders($response), + $response->getStatusCode(), + $bodyDigest, + $currentTime + $ttl + )); + + $this->cache->save($key, serialize($entries)); + } + + public function delete(RequestInterface $request) + { + $key = $this->getCacheKey($request); + if ($entries = $this->cache->fetch($key)) { + // Delete each cached body + foreach (unserialize($entries) as $entry) { + if ($entry[3]) { + $this->cache->delete($entry[3]); + } + } + $this->cache->delete($key); + } + } + + public function purge($url) + { + foreach (array('GET', 'HEAD', 'POST', 'PUT', 'DELETE') as $method) { + $this->delete(new Request($method, $url)); + } + } + + public function fetch(RequestInterface $request) + { + $key = $this->getCacheKey($request); + if (!($entries = $this->cache->fetch($key))) { + return null; + } + + $match = null; + $headers = $this->persistHeaders($request); + $entries = unserialize($entries); + foreach ($entries as $index => $entry) { + if ($this->requestsMatch(isset($entry[1]['vary']) ? $entry[1]['vary'] : '', $headers, $entry[0])) { + $match = $entry; + break; + } + } + + if (!$match) { + return null; + } + + // Ensure that the response is not expired + $response = null; + if ($match[4] < time()) { + $response = -1; + } else { + $response = new Response($match[2], $match[1]); + if ($match[3]) { + if ($body = $this->cache->fetch($match[3])) { + $response->setBody($body); + } else { + // The response is not valid because the body was somehow deleted + $response = -1; + } + } + } + + if ($response === -1) { + // Remove the entry from the metadata and update the cache + unset($entries[$index]); + if ($entries) { + $this->cache->save($key, serialize($entries)); + } else { + $this->cache->delete($key); + } + return null; + } + + return $response; + } + + /** + * Hash a request URL into a string that returns cache metadata + * + * @param RequestInterface $request + * + * @return string + */ + protected function getCacheKey(RequestInterface $request) + { + // Allow cache.key_filter to trim down the URL cache key by removing generate query string values (e.g. auth) + if ($filter = $request->getParams()->get('cache.key_filter')) { + $url = $request->getUrl(true); + foreach (explode(',', $filter) as $remove) { + $url->getQuery()->remove(trim($remove)); + } + } else { + $url = $request->getUrl(); + } + + return $this->keyPrefix . md5($request->getMethod() . ' ' . $url); + } + + /** + * Create a cache key for a response's body + * + * @param string $url URL of the entry + * @param EntityBodyInterface $body Response body + * + * @return string + */ + protected function getBodyKey($url, EntityBodyInterface $body) + { + return $this->keyPrefix . md5($url) . $body->getContentMd5(); + } + + /** + * Determines whether two Request HTTP header sets are non-varying + * + * @param string $vary Response vary header + * @param array $r1 HTTP header array + * @param array $r2 HTTP header array + * + * @return bool + */ + private function requestsMatch($vary, $r1, $r2) + { + if ($vary) { + foreach (explode(',', $vary) as $header) { + $key = trim(strtolower($header)); + $v1 = isset($r1[$key]) ? $r1[$key] : null; + $v2 = isset($r2[$key]) ? $r2[$key] : null; + if ($v1 !== $v2) { + return false; + } + } + } + + return true; + } + + /** + * Creates an array of cacheable and normalized message headers + * + * @param MessageInterface $message + * + * @return array + */ + private function persistHeaders(MessageInterface $message) + { + // Headers are excluded from the caching (see RFC 2616:13.5.1) + static $noCache = array( + 'age' => true, + 'connection' => true, + 'keep-alive' => true, + 'proxy-authenticate' => true, + 'proxy-authorization' => true, + 'te' => true, + 'trailers' => true, + 'transfer-encoding' => true, + 'upgrade' => true, + 'set-cookie' => true, + 'set-cookie2' => true + ); + + // Clone the response to not destroy any necessary headers when caching + $headers = $message->getHeaders()->getAll(); + $headers = array_diff_key($headers, $noCache); + // Cast the headers to a string + $headers = array_map(function ($h) { return (string) $h; }, $headers); + + return $headers; + } +} diff --git a/guzzle/guzzle/src/Guzzle/Plugin/Cache/DefaultCanCacheStrategy.php b/guzzle/guzzle/src/Guzzle/Plugin/Cache/DefaultCanCacheStrategy.php new file mode 100644 index 000000000..3ca1fbf19 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Plugin/Cache/DefaultCanCacheStrategy.php @@ -0,0 +1,32 @@ +getMethod() != RequestInterface::GET && $request->getMethod() != RequestInterface::HEAD) { + return false; + } + + // Never cache requests when using no-store + if ($request->hasHeader('Cache-Control') && $request->getHeader('Cache-Control')->hasDirective('no-store')) { + return false; + } + + return true; + } + + public function canCacheResponse(Response $response) + { + return $response->isSuccessful() && $response->canCache(); + } +} diff --git a/guzzle/guzzle/src/Guzzle/Plugin/Cache/DefaultRevalidation.php b/guzzle/guzzle/src/Guzzle/Plugin/Cache/DefaultRevalidation.php new file mode 100644 index 000000000..ab2e2a2ce --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Plugin/Cache/DefaultRevalidation.php @@ -0,0 +1,172 @@ +storage = $cache; + $this->canCache = $canCache ?: new DefaultCanCacheStrategy(); + } + + public function revalidate(RequestInterface $request, Response $response) + { + try { + $revalidate = $this->createRevalidationRequest($request, $response); + $validateResponse = $revalidate->send(); + if ($validateResponse->getStatusCode() == 200) { + return $this->handle200Response($request, $validateResponse); + } elseif ($validateResponse->getStatusCode() == 304) { + return $this->handle304Response($request, $validateResponse, $response); + } + } catch (BadResponseException $e) { + $this->handleBadResponse($e); + } + + // Other exceptions encountered in the revalidation request are ignored + // in hopes that sending a request to the origin server will fix it + return false; + } + + public function shouldRevalidate(RequestInterface $request, Response $response) + { + if ($request->getMethod() != RequestInterface::GET) { + return false; + } + + $reqCache = $request->getHeader('Cache-Control'); + $resCache = $response->getHeader('Cache-Control'); + + $revalidate = $request->getHeader('Pragma') == 'no-cache' || + ($reqCache && ($reqCache->hasDirective('no-cache') || $reqCache->hasDirective('must-revalidate'))) || + ($resCache && ($resCache->hasDirective('no-cache') || $resCache->hasDirective('must-revalidate'))); + + // Use the strong ETag validator if available and the response contains no Cache-Control directive + if (!$revalidate && !$resCache && $response->hasHeader('ETag')) { + $revalidate = true; + } + + return $revalidate; + } + + /** + * Handles a bad response when attempting to revalidate + * + * @param BadResponseException $e Exception encountered + * + * @throws BadResponseException + */ + protected function handleBadResponse(BadResponseException $e) + { + // 404 errors mean the resource no longer exists, so remove from + // cache, and prevent an additional request by throwing the exception + if ($e->getResponse()->getStatusCode() == 404) { + $this->storage->delete($e->getRequest()); + throw $e; + } + } + + /** + * Creates a request to use for revalidation + * + * @param RequestInterface $request Request + * @param Response $response Response to revalidate + * + * @return RequestInterface returns a revalidation request + */ + protected function createRevalidationRequest(RequestInterface $request, Response $response) + { + $revalidate = clone $request; + $revalidate->removeHeader('Pragma') + ->removeHeader('Cache-Control') + ->setHeader('If-Modified-Since', $response->getLastModified() ?: $response->getDate()); + + if ($response->getEtag()) { + $revalidate->setHeader('If-None-Match', $response->getEtag()); + } + + // Remove any cache plugins that might be on the request to prevent infinite recursive revalidations + $dispatcher = $revalidate->getEventDispatcher(); + foreach ($dispatcher->getListeners() as $eventName => $listeners) { + foreach ($listeners as $listener) { + if (is_array($listener) && $listener[0] instanceof CachePlugin) { + $dispatcher->removeListener($eventName, $listener); + } + } + } + + return $revalidate; + } + + /** + * Handles a 200 response response from revalidating. The server does not support validation, so use this response. + * + * @param RequestInterface $request Request that was sent + * @param Response $validateResponse Response received + * + * @return bool Returns true if valid, false if invalid + */ + protected function handle200Response(RequestInterface $request, Response $validateResponse) + { + $request->setResponse($validateResponse); + if ($this->canCache->canCacheResponse($validateResponse)) { + $this->storage->cache($request, $validateResponse); + } + + return false; + } + + /** + * Handle a 304 response and ensure that it is still valid + * + * @param RequestInterface $request Request that was sent + * @param Response $validateResponse Response received + * @param Response $response Original cached response + * + * @return bool Returns true if valid, false if invalid + */ + protected function handle304Response(RequestInterface $request, Response $validateResponse, Response $response) + { + static $replaceHeaders = array('Date', 'Expires', 'Cache-Control', 'ETag', 'Last-Modified'); + + // Make sure that this response has the same ETag + if ($validateResponse->getEtag() != $response->getEtag()) { + return false; + } + + // Replace cached headers with any of these headers from the + // origin server that might be more up to date + $modified = false; + foreach ($replaceHeaders as $name) { + if ($validateResponse->hasHeader($name)) { + $modified = true; + $response->setHeader($name, $validateResponse->getHeader($name)); + } + } + + // Store the updated response in cache + if ($modified && $this->canCache->canCacheResponse($response)) { + $this->storage->cache($request, $response); + } + + return true; + } +} diff --git a/guzzle/guzzle/src/Guzzle/Plugin/Cache/DenyRevalidation.php b/guzzle/guzzle/src/Guzzle/Plugin/Cache/DenyRevalidation.php new file mode 100644 index 000000000..88b86f3ca --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Plugin/Cache/DenyRevalidation.php @@ -0,0 +1,19 @@ +=5.3.2", + "guzzle/http": "self.version", + "guzzle/cache": "self.version" + }, + "autoload": { + "psr-0": { "Guzzle\\Plugin\\Cache": "" } + }, + "target-dir": "Guzzle/Plugin/Cache", + "extra": { + "branch-alias": { + "dev-master": "3.7-dev" + } + } +} diff --git a/guzzle/guzzle/src/Guzzle/Plugin/Cookie/Cookie.php b/guzzle/guzzle/src/Guzzle/Plugin/Cookie/Cookie.php new file mode 100644 index 000000000..f5803dcb1 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Plugin/Cookie/Cookie.php @@ -0,0 +1,513 @@ + '', + 'value' => '', + 'domain' => '', + 'path' => '/', + 'expires' => null, + 'max_age' => 0, + 'comment' => null, + 'comment_url' => null, + 'port' => array(), + 'version' => null, + 'secure' => false, + 'discard' => false, + 'http_only' => false + ); + + $this->data = array_merge($defaults, $data); + // Extract the expires value and turn it into a UNIX timestamp if needed + if (!$this->getExpires() && $this->getMaxAge()) { + // Calculate the expires date + $this->setExpires(time() + (int) $this->getMaxAge()); + } elseif ($this->getExpires() && !is_numeric($this->getExpires())) { + $this->setExpires(strtotime($this->getExpires())); + } + } + + /** + * Get the cookie as an array + * + * @return array + */ + public function toArray() + { + return $this->data; + } + + /** + * Get the cookie name + * + * @return string + */ + public function getName() + { + return $this->data['name']; + } + + /** + * Set the cookie name + * + * @param string $name Cookie name + * + * @return Cookie + */ + public function setName($name) + { + return $this->setData('name', $name); + } + + /** + * Get the cookie value + * + * @return string + */ + public function getValue() + { + return $this->data['value']; + } + + /** + * Set the cookie value + * + * @param string $value Cookie value + * + * @return Cookie + */ + public function setValue($value) + { + return $this->setData('value', $value); + } + + /** + * Get the domain + * + * @return string|null + */ + public function getDomain() + { + return $this->data['domain']; + } + + /** + * Set the domain of the cookie + * + * @param string $domain + * + * @return Cookie + */ + public function setDomain($domain) + { + return $this->setData('domain', $domain); + } + + /** + * Get the path + * + * @return string + */ + public function getPath() + { + return $this->data['path']; + } + + /** + * Set the path of the cookie + * + * @param string $path Path of the cookie + * + * @return Cookie + */ + public function setPath($path) + { + return $this->setData('path', $path); + } + + /** + * Maximum lifetime of the cookie in seconds + * + * @return int|null + */ + public function getMaxAge() + { + return $this->data['max_age']; + } + + /** + * Set the max-age of the cookie + * + * @param int $maxAge Max age of the cookie in seconds + * + * @return Cookie + */ + public function setMaxAge($maxAge) + { + return $this->setData('max_age', $maxAge); + } + + /** + * The UNIX timestamp when the cookie expires + * + * @return mixed + */ + public function getExpires() + { + return $this->data['expires']; + } + + /** + * Set the unix timestamp for which the cookie will expire + * + * @param int $timestamp Unix timestamp + * + * @return Cookie + */ + public function setExpires($timestamp) + { + return $this->setData('expires', $timestamp); + } + + /** + * Version of the cookie specification. RFC 2965 is 1 + * + * @return mixed + */ + public function getVersion() + { + return $this->data['version']; + } + + /** + * Set the cookie version + * + * @param string|int $version Version to set + * + * @return Cookie + */ + public function setVersion($version) + { + return $this->setData('version', $version); + } + + /** + * Get whether or not this is a secure cookie + * + * @return null|bool + */ + public function getSecure() + { + return $this->data['secure']; + } + + /** + * Set whether or not the cookie is secure + * + * @param bool $secure Set to true or false if secure + * + * @return Cookie + */ + public function setSecure($secure) + { + return $this->setData('secure', (bool) $secure); + } + + /** + * Get whether or not this is a session cookie + * + * @return null|bool + */ + public function getDiscard() + { + return $this->data['discard']; + } + + /** + * Set whether or not this is a session cookie + * + * @param bool $discard Set to true or false if this is a session cookie + * + * @return Cookie + */ + public function setDiscard($discard) + { + return $this->setData('discard', $discard); + } + + /** + * Get the comment + * + * @return string|null + */ + public function getComment() + { + return $this->data['comment']; + } + + /** + * Set the comment of the cookie + * + * @param string $comment Cookie comment + * + * @return Cookie + */ + public function setComment($comment) + { + return $this->setData('comment', $comment); + } + + /** + * Get the comment URL of the cookie + * + * @return string|null + */ + public function getCommentUrl() + { + return $this->data['comment_url']; + } + + /** + * Set the comment URL of the cookie + * + * @param string $commentUrl Cookie comment URL for more information + * + * @return Cookie + */ + public function setCommentUrl($commentUrl) + { + return $this->setData('comment_url', $commentUrl); + } + + /** + * Get an array of acceptable ports this cookie can be used with + * + * @return array + */ + public function getPorts() + { + return $this->data['port']; + } + + /** + * Set a list of acceptable ports this cookie can be used with + * + * @param array $ports Array of acceptable ports + * + * @return Cookie + */ + public function setPorts(array $ports) + { + return $this->setData('port', $ports); + } + + /** + * Get whether or not this is an HTTP only cookie + * + * @return bool + */ + public function getHttpOnly() + { + return $this->data['http_only']; + } + + /** + * Set whether or not this is an HTTP only cookie + * + * @param bool $httpOnly Set to true or false if this is HTTP only + * + * @return Cookie + */ + public function setHttpOnly($httpOnly) + { + return $this->setData('http_only', $httpOnly); + } + + /** + * Get an array of extra cookie data + * + * @return array + */ + public function getAttributes() + { + return $this->data['data']; + } + + /** + * Get a specific data point from the extra cookie data + * + * @param string $name Name of the data point to retrieve + * + * @return null|string + */ + public function getAttribute($name) + { + return array_key_exists($name, $this->data['data']) ? $this->data['data'][$name] : null; + } + + /** + * Set a cookie data attribute + * + * @param string $name Name of the attribute to set + * @param string $value Value to set + * + * @return Cookie + */ + public function setAttribute($name, $value) + { + $this->data['data'][$name] = $value; + + return $this; + } + + /** + * Check if the cookie matches a path value + * + * @param string $path Path to check against + * + * @return bool + */ + public function matchesPath($path) + { + return !$this->getPath() || 0 === stripos($path, $this->getPath()); + } + + /** + * Check if the cookie matches a domain value + * + * @param string $domain Domain to check against + * + * @return bool + */ + public function matchesDomain($domain) + { + // Remove the leading '.' as per spec in RFC 6265: http://tools.ietf.org/html/rfc6265#section-5.2.3 + $cookieDomain = ltrim($this->getDomain(), '.'); + + // Domain not set or exact match. + if (!$cookieDomain || !strcasecmp($domain, $cookieDomain)) { + return true; + } + + // Matching the subdomain according to RFC 6265: http://tools.ietf.org/html/rfc6265#section-5.1.3 + if (filter_var($domain, FILTER_VALIDATE_IP)) { + return false; + } + + return (bool) preg_match('/\.' . preg_quote($cookieDomain, '/') . '$/i', $domain); + } + + /** + * Check if the cookie is compatible with a specific port + * + * @param int $port Port to check + * + * @return bool + */ + public function matchesPort($port) + { + return count($this->getPorts()) == 0 || in_array($port, $this->getPorts()); + } + + /** + * Check if the cookie is expired + * + * @return bool + */ + public function isExpired() + { + return $this->getExpires() && time() > $this->getExpires(); + } + + /** + * Check if the cookie is valid according to RFC 6265 + * + * @return bool|string Returns true if valid or an error message if invalid + */ + public function validate() + { + // Names must not be empty, but can be 0 + $name = $this->getName(); + if (empty($name) && !is_numeric($name)) { + return 'The cookie name must not be empty'; + } + + // Check if any of the invalid characters are present in the cookie name + if (strpbrk($name, self::getInvalidCharacters()) !== false) { + return 'The cookie name must not contain invalid characters: ' . $name; + } + + // Value must not be empty, but can be 0 + $value = $this->getValue(); + if (empty($value) && !is_numeric($value)) { + return 'The cookie value must not be empty'; + } + + // Domains must not be empty, but can be 0 + // A "0" is not a valid internet domain, but may be used as server name in a private network + $domain = $this->getDomain(); + if (empty($domain) && !is_numeric($domain)) { + return 'The cookie domain must not be empty'; + } + + return true; + } + + /** + * Set a value and return the cookie object + * + * @param string $key Key to set + * @param string $value Value to set + * + * @return Cookie + */ + private function setData($key, $value) + { + $this->data[$key] = $value; + + return $this; + } +} diff --git a/guzzle/guzzle/src/Guzzle/Plugin/Cookie/CookieJar/ArrayCookieJar.php b/guzzle/guzzle/src/Guzzle/Plugin/Cookie/CookieJar/ArrayCookieJar.php new file mode 100644 index 000000000..68d5be7e6 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Plugin/Cookie/CookieJar/ArrayCookieJar.php @@ -0,0 +1,222 @@ +strictMode = $strictMode; + } + + /** + * Enable or disable strict mode on the cookie jar + * + * @param bool $strictMode Set to true to throw exceptions when invalid cookies are added. False to ignore them. + * + * @return self + */ + public function setStrictMode($strictMode) + { + $this->strictMode = $strictMode; + } + + public function remove($domain = null, $path = null, $name = null) + { + $cookies = $this->all($domain, $path, $name, false, false); + $this->cookies = array_filter($this->cookies, function (Cookie $cookie) use ($cookies) { + return !in_array($cookie, $cookies, true); + }); + + return $this; + } + + public function removeTemporary() + { + $this->cookies = array_filter($this->cookies, function (Cookie $cookie) { + return !$cookie->getDiscard() && $cookie->getExpires(); + }); + + return $this; + } + + public function removeExpired() + { + $currentTime = time(); + $this->cookies = array_filter($this->cookies, function (Cookie $cookie) use ($currentTime) { + return !$cookie->getExpires() || $currentTime < $cookie->getExpires(); + }); + + return $this; + } + + public function all($domain = null, $path = null, $name = null, $skipDiscardable = false, $skipExpired = true) + { + return array_values(array_filter($this->cookies, function (Cookie $cookie) use ( + $domain, + $path, + $name, + $skipDiscardable, + $skipExpired + ) { + return false === (($name && $cookie->getName() != $name) || + ($skipExpired && $cookie->isExpired()) || + ($skipDiscardable && ($cookie->getDiscard() || !$cookie->getExpires())) || + ($path && !$cookie->matchesPath($path)) || + ($domain && !$cookie->matchesDomain($domain))); + })); + } + + public function add(Cookie $cookie) + { + // Only allow cookies with set and valid domain, name, value + $result = $cookie->validate(); + if ($result !== true) { + if ($this->strictMode) { + throw new InvalidCookieException($result); + } else { + return false; + } + } + + // Resolve conflicts with previously set cookies + foreach ($this->cookies as $i => $c) { + + // Two cookies are identical, when their path, domain, port and name are identical + if ($c->getPath() != $cookie->getPath() || + $c->getDomain() != $cookie->getDomain() || + $c->getPorts() != $cookie->getPorts() || + $c->getName() != $cookie->getName() + ) { + continue; + } + + // The previously set cookie is a discard cookie and this one is not so allow the new cookie to be set + if (!$cookie->getDiscard() && $c->getDiscard()) { + unset($this->cookies[$i]); + continue; + } + + // If the new cookie's expiration is further into the future, then replace the old cookie + if ($cookie->getExpires() > $c->getExpires()) { + unset($this->cookies[$i]); + continue; + } + + // If the value has changed, we better change it + if ($cookie->getValue() !== $c->getValue()) { + unset($this->cookies[$i]); + continue; + } + + // The cookie exists, so no need to continue + return false; + } + + $this->cookies[] = $cookie; + + return true; + } + + /** + * Serializes the cookie cookieJar + * + * @return string + */ + public function serialize() + { + // Only serialize long term cookies and unexpired cookies + return json_encode(array_map(function (Cookie $cookie) { + return $cookie->toArray(); + }, $this->all(null, null, null, true, true))); + } + + /** + * Unserializes the cookie cookieJar + */ + public function unserialize($data) + { + $data = json_decode($data, true); + if (empty($data)) { + $this->cookies = array(); + } else { + $this->cookies = array_map(function (array $cookie) { + return new Cookie($cookie); + }, $data); + } + } + + /** + * Returns the total number of stored cookies + * + * @return int + */ + public function count() + { + return count($this->cookies); + } + + /** + * Returns an iterator + * + * @return \ArrayIterator + */ + public function getIterator() + { + return new \ArrayIterator($this->cookies); + } + + public function addCookiesFromResponse(Response $response, RequestInterface $request = null) + { + if ($cookieHeader = $response->getHeader('Set-Cookie')) { + $parser = ParserRegistry::getInstance()->getParser('cookie'); + foreach ($cookieHeader as $cookie) { + if ($parsed = $request + ? $parser->parseCookie($cookie, $request->getHost(), $request->getPath()) + : $parser->parseCookie($cookie) + ) { + // Break up cookie v2 into multiple cookies + foreach ($parsed['cookies'] as $key => $value) { + $row = $parsed; + $row['name'] = $key; + $row['value'] = $value; + unset($row['cookies']); + $this->add(new Cookie($row)); + } + } + } + } + } + + public function getMatchingCookies(RequestInterface $request) + { + // Find cookies that match this request + $cookies = $this->all($request->getHost(), $request->getPath()); + // Remove ineligible cookies + foreach ($cookies as $index => $cookie) { + if (!$cookie->matchesPort($request->getPort()) || ($cookie->getSecure() && $request->getScheme() != 'https')) { + unset($cookies[$index]); + } + }; + + return $cookies; + } +} diff --git a/guzzle/guzzle/src/Guzzle/Plugin/Cookie/CookieJar/CookieJarInterface.php b/guzzle/guzzle/src/Guzzle/Plugin/Cookie/CookieJar/CookieJarInterface.php new file mode 100644 index 000000000..7faa7d21f --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Plugin/Cookie/CookieJar/CookieJarInterface.php @@ -0,0 +1,85 @@ +filename = $cookieFile; + $this->load(); + } + + /** + * Saves the file when shutting down + */ + public function __destruct() + { + $this->persist(); + } + + /** + * Save the contents of the data array to the file + * + * @throws RuntimeException if the file cannot be found or created + */ + protected function persist() + { + if (false === file_put_contents($this->filename, $this->serialize())) { + // @codeCoverageIgnoreStart + throw new RuntimeException('Unable to open file ' . $this->filename); + // @codeCoverageIgnoreEnd + } + } + + /** + * Load the contents of the json formatted file into the data array and discard any unsaved state + */ + protected function load() + { + $json = file_get_contents($this->filename); + if (false === $json) { + // @codeCoverageIgnoreStart + throw new RuntimeException('Unable to open file ' . $this->filename); + // @codeCoverageIgnoreEnd + } + + $this->unserialize($json); + $this->cookies = $this->cookies ?: array(); + } +} diff --git a/guzzle/guzzle/src/Guzzle/Plugin/Cookie/CookiePlugin.php b/guzzle/guzzle/src/Guzzle/Plugin/Cookie/CookiePlugin.php new file mode 100644 index 000000000..df3210ee1 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Plugin/Cookie/CookiePlugin.php @@ -0,0 +1,70 @@ +cookieJar = $cookieJar ?: new ArrayCookieJar(); + } + + public static function getSubscribedEvents() + { + return array( + 'request.before_send' => array('onRequestBeforeSend', 125), + 'request.sent' => array('onRequestSent', 125) + ); + } + + /** + * Get the cookie cookieJar + * + * @return CookieJarInterface + */ + public function getCookieJar() + { + return $this->cookieJar; + } + + /** + * Add cookies before a request is sent + * + * @param Event $event + */ + public function onRequestBeforeSend(Event $event) + { + $request = $event['request']; + if (!$request->getParams()->get('cookies.disable')) { + $request->removeHeader('Cookie'); + // Find cookies that match this request + foreach ($this->cookieJar->getMatchingCookies($request) as $cookie) { + $request->addCookie($cookie->getName(), $cookie->getValue()); + } + } + } + + /** + * Extract cookies from a sent request + * + * @param Event $event + */ + public function onRequestSent(Event $event) + { + $this->cookieJar->addCookiesFromResponse($event['response'], $event['request']); + } +} diff --git a/guzzle/guzzle/src/Guzzle/Plugin/Cookie/Exception/InvalidCookieException.php b/guzzle/guzzle/src/Guzzle/Plugin/Cookie/Exception/InvalidCookieException.php new file mode 100644 index 000000000..b1fa6fd89 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Plugin/Cookie/Exception/InvalidCookieException.php @@ -0,0 +1,7 @@ +=5.3.2", + "guzzle/http": "self.version" + }, + "autoload": { + "psr-0": { "Guzzle\\Plugin\\Cookie": "" } + }, + "target-dir": "Guzzle/Plugin/Cookie", + "extra": { + "branch-alias": { + "dev-master": "3.7-dev" + } + } +} diff --git a/guzzle/guzzle/src/Guzzle/Plugin/CurlAuth/CurlAuthPlugin.php b/guzzle/guzzle/src/Guzzle/Plugin/CurlAuth/CurlAuthPlugin.php new file mode 100644 index 000000000..610e60cad --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Plugin/CurlAuth/CurlAuthPlugin.php @@ -0,0 +1,46 @@ +getConfig()->setPath('request.options/auth', array('user', 'pass', 'Basic|Digest'); + */ +class CurlAuthPlugin implements EventSubscriberInterface +{ + private $username; + private $password; + private $scheme; + + /** + * @param string $username HTTP basic auth username + * @param string $password Password + * @param int $scheme Curl auth scheme + */ + public function __construct($username, $password, $scheme=CURLAUTH_BASIC) + { + Version::warn(__CLASS__ . " is deprecated. Use \$client->getConfig()->setPath('request.options/auth', array('user', 'pass', 'Basic|Digest');"); + $this->username = $username; + $this->password = $password; + $this->scheme = $scheme; + } + + public static function getSubscribedEvents() + { + return array('client.create_request' => array('onRequestCreate', 255)); + } + + /** + * Add basic auth + * + * @param Event $event + */ + public function onRequestCreate(Event $event) + { + $event['request']->setAuth($this->username, $this->password, $this->scheme); + } +} diff --git a/guzzle/guzzle/src/Guzzle/Plugin/CurlAuth/composer.json b/guzzle/guzzle/src/Guzzle/Plugin/CurlAuth/composer.json new file mode 100644 index 000000000..edc8b24e5 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Plugin/CurlAuth/composer.json @@ -0,0 +1,27 @@ +{ + "name": "guzzle/plugin-curlauth", + "description": "Guzzle cURL authorization plugin", + "homepage": "http://guzzlephp.org/", + "keywords": ["plugin", "curl", "guzzle"], + "license": "MIT", + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "require": { + "php": ">=5.3.2", + "guzzle/http": "self.version" + }, + "autoload": { + "psr-0": { "Guzzle\\Plugin\\CurlAuth": "" } + }, + "target-dir": "Guzzle/Plugin/CurlAuth", + "extra": { + "branch-alias": { + "dev-master": "3.7-dev" + } + } +} diff --git a/guzzle/guzzle/src/Guzzle/Plugin/ErrorResponse/ErrorResponseExceptionInterface.php b/guzzle/guzzle/src/Guzzle/Plugin/ErrorResponse/ErrorResponseExceptionInterface.php new file mode 100644 index 000000000..5dce8bd6c --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Plugin/ErrorResponse/ErrorResponseExceptionInterface.php @@ -0,0 +1,22 @@ + array('onCommandBeforeSend', -1)); + } + + /** + * Adds a listener to requests before they sent from a command + * + * @param Event $event Event emitted + */ + public function onCommandBeforeSend(Event $event) + { + $command = $event['command']; + if ($operation = $command->getOperation()) { + if ($operation->getErrorResponses()) { + $request = $command->getRequest(); + $request->getEventDispatcher() + ->addListener('request.complete', $this->getErrorClosure($request, $command, $operation)); + } + } + } + + /** + * @param RequestInterface $request Request that received an error + * @param CommandInterface $command Command that created the request + * @param Operation $operation Operation that defines the request and errors + * + * @return \Closure Returns a closure + * @throws ErrorResponseException + */ + protected function getErrorClosure(RequestInterface $request, CommandInterface $command, Operation $operation) + { + return function (Event $event) use ($request, $command, $operation) { + $response = $event['response']; + foreach ($operation->getErrorResponses() as $error) { + if (!isset($error['class'])) { + continue; + } + if (isset($error['code']) && $response->getStatusCode() != $error['code']) { + continue; + } + if (isset($error['reason']) && $response->getReasonPhrase() != $error['reason']) { + continue; + } + $className = $error['class']; + $errorClassInterface = __NAMESPACE__ . '\\ErrorResponseExceptionInterface'; + if (!class_exists($className)) { + throw new ErrorResponseException("{$className} does not exist"); + } elseif (!is_subclass_of($className, $errorClassInterface)) { + throw new ErrorResponseException("{$className} must implement {$errorClassInterface}"); + } + throw $className::fromCommand($command, $response); + } + }; + } +} diff --git a/guzzle/guzzle/src/Guzzle/Plugin/ErrorResponse/Exception/ErrorResponseException.php b/guzzle/guzzle/src/Guzzle/Plugin/ErrorResponse/Exception/ErrorResponseException.php new file mode 100644 index 000000000..1d89e40e7 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Plugin/ErrorResponse/Exception/ErrorResponseException.php @@ -0,0 +1,7 @@ +=5.3.2", + "guzzle/service": "self.version" + }, + "autoload": { + "psr-0": { "Guzzle\\Plugin\\ErrorResponse": "" } + }, + "target-dir": "Guzzle/Plugin/ErrorResponse", + "extra": { + "branch-alias": { + "dev-master": "3.7-dev" + } + } +} diff --git a/guzzle/guzzle/src/Guzzle/Plugin/History/HistoryPlugin.php b/guzzle/guzzle/src/Guzzle/Plugin/History/HistoryPlugin.php new file mode 100644 index 000000000..7375e892b --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Plugin/History/HistoryPlugin.php @@ -0,0 +1,163 @@ + array('onRequestSent', 9999)); + } + + /** + * Convert to a string that contains all request and response headers + * + * @return string + */ + public function __toString() + { + $lines = array(); + foreach ($this->transactions as $entry) { + $response = isset($entry['response']) ? $entry['response'] : ''; + $lines[] = '> ' . trim($entry['request']) . "\n\n< " . trim($response) . "\n"; + } + + return implode("\n", $lines); + } + + /** + * Add a request to the history + * + * @param RequestInterface $request Request to add + * @param Response $response Response of the request + * + * @return HistoryPlugin + */ + public function add(RequestInterface $request, Response $response = null) + { + if (!$response && $request->getResponse()) { + $response = $request->getResponse(); + } + + $this->transactions[] = array('request' => $request, 'response' => $response); + if (count($this->transactions) > $this->getlimit()) { + array_shift($this->transactions); + } + + return $this; + } + + /** + * Set the max number of requests to store + * + * @param int $limit Limit + * + * @return HistoryPlugin + */ + public function setLimit($limit) + { + $this->limit = (int) $limit; + + return $this; + } + + /** + * Get the request limit + * + * @return int + */ + public function getLimit() + { + return $this->limit; + } + + /** + * Get all of the raw transactions in the form of an array of associative arrays containing + * 'request' and 'response' keys. + * + * @return array + */ + public function getAll() + { + return $this->transactions; + } + + /** + * Get the requests in the history + * + * @return \ArrayIterator + */ + public function getIterator() + { + // Return an iterator just like the old iteration of the HistoryPlugin for BC compatibility (use getAll()) + return new \ArrayIterator(array_map(function ($entry) { + $entry['request']->getParams()->set('actual_response', $entry['response']); + return $entry['request']; + }, $this->transactions)); + } + + /** + * Get the number of requests in the history + * + * @return int + */ + public function count() + { + return count($this->transactions); + } + + /** + * Get the last request sent + * + * @return RequestInterface + */ + public function getLastRequest() + { + $last = end($this->transactions); + + return $last['request']; + } + + /** + * Get the last response in the history + * + * @return Response|null + */ + public function getLastResponse() + { + $last = end($this->transactions); + + return isset($last['response']) ? $last['response'] : null; + } + + /** + * Clears the history + * + * @return HistoryPlugin + */ + public function clear() + { + $this->transactions = array(); + + return $this; + } + + public function onRequestSent(Event $event) + { + $this->add($event['request'], $event['response']); + } +} diff --git a/guzzle/guzzle/src/Guzzle/Plugin/History/composer.json b/guzzle/guzzle/src/Guzzle/Plugin/History/composer.json new file mode 100644 index 000000000..ba0bf2c4d --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Plugin/History/composer.json @@ -0,0 +1,27 @@ +{ + "name": "guzzle/plugin-history", + "description": "Guzzle history plugin", + "homepage": "http://guzzlephp.org/", + "keywords": ["plugin", "guzzle"], + "license": "MIT", + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "require": { + "php": ">=5.3.2", + "guzzle/http": "self.version" + }, + "autoload": { + "psr-0": { "Guzzle\\Plugin\\History": "" } + }, + "target-dir": "Guzzle/Plugin/History", + "extra": { + "branch-alias": { + "dev-master": "3.7-dev" + } + } +} diff --git a/guzzle/guzzle/src/Guzzle/Plugin/Log/LogPlugin.php b/guzzle/guzzle/src/Guzzle/Plugin/Log/LogPlugin.php new file mode 100644 index 000000000..cabdea854 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Plugin/Log/LogPlugin.php @@ -0,0 +1,161 @@ +logAdapter = $logAdapter; + $this->formatter = $formatter instanceof MessageFormatter ? $formatter : new MessageFormatter($formatter); + $this->wireBodies = $wireBodies; + } + + /** + * Get a log plugin that outputs full request, response, and curl error information to stderr + * + * @param bool $wireBodies Set to false to disable request/response body output when they use are not repeatable + * @param resource $stream Stream to write to when logging. Defaults to STDERR when it is available + * + * @return self + */ + public static function getDebugPlugin($wireBodies = true, $stream = null) + { + if ($stream === null) { + if (defined('STDERR')) { + $stream = STDERR; + } else { + $stream = fopen('php://output', 'w'); + } + } + + return new self(new ClosureLogAdapter(function ($m) use ($stream) { + fwrite($stream, $m . PHP_EOL); + }), "# Request:\n{request}\n\n# Response:\n{response}\n\n# Errors: {curl_code} {curl_error}", $wireBodies); + } + + public static function getSubscribedEvents() + { + return array( + 'curl.callback.write' => array('onCurlWrite', 255), + 'curl.callback.read' => array('onCurlRead', 255), + 'request.before_send' => array('onRequestBeforeSend', 255), + 'request.sent' => array('onRequestSent', 255) + ); + } + + /** + * Event triggered when curl data is read from a request + * + * @param Event $event + */ + public function onCurlRead(Event $event) + { + // Stream the request body to the log if the body is not repeatable + if ($wire = $event['request']->getParams()->get('request_wire')) { + $wire->write($event['read']); + } + } + + /** + * Event triggered when curl data is written to a response + * + * @param Event $event + */ + public function onCurlWrite(Event $event) + { + // Stream the response body to the log if the body is not repeatable + if ($wire = $event['request']->getParams()->get('response_wire')) { + $wire->write($event['write']); + } + } + + /** + * Called before a request is sent + * + * @param Event $event + */ + public function onRequestBeforeSend(Event $event) + { + if ($this->wireBodies) { + $request = $event['request']; + // Ensure that curl IO events are emitted + $request->getCurlOptions()->set('emit_io', true); + // We need to make special handling for content wiring and non-repeatable streams. + if ($request instanceof EntityEnclosingRequestInterface && $request->getBody() + && (!$request->getBody()->isSeekable() || !$request->getBody()->isReadable()) + ) { + // The body of the request cannot be recalled so logging the body will require us to buffer it + $request->getParams()->set('request_wire', EntityBody::factory()); + } + if (!$request->getResponseBody()->isRepeatable()) { + // The body of the response cannot be recalled so logging the body will require us to buffer it + $request->getParams()->set('response_wire', EntityBody::factory()); + } + } + } + + /** + * Triggers the actual log write when a request completes + * + * @param Event $event + */ + public function onRequestSent(Event $event) + { + $request = $event['request']; + $response = $event['response']; + $handle = $event['handle']; + + if ($wire = $request->getParams()->get('request_wire')) { + $request = clone $request; + $request->setBody($wire); + } + + if ($wire = $request->getParams()->get('response_wire')) { + $response = clone $response; + $response->setBody($wire); + } + + // Send the log message to the adapter, adding a category and host + $priority = $response && $response->isError() ? LOG_ERR : LOG_DEBUG; + $message = $this->formatter->format($request, $response, $handle); + $this->logAdapter->log($message, $priority, array( + 'request' => $request, + 'response' => $response, + 'handle' => $handle + )); + } +} diff --git a/guzzle/guzzle/src/Guzzle/Plugin/Log/composer.json b/guzzle/guzzle/src/Guzzle/Plugin/Log/composer.json new file mode 100644 index 000000000..130e6da0a --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Plugin/Log/composer.json @@ -0,0 +1,28 @@ +{ + "name": "guzzle/plugin-log", + "description": "Guzzle log plugin for over the wire logging", + "homepage": "http://guzzlephp.org/", + "keywords": ["plugin", "log", "guzzle"], + "license": "MIT", + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "require": { + "php": ">=5.3.2", + "guzzle/http": "self.version", + "guzzle/log": "self.version" + }, + "autoload": { + "psr-0": { "Guzzle\\Plugin\\Log": "" } + }, + "target-dir": "Guzzle/Plugin/Log", + "extra": { + "branch-alias": { + "dev-master": "3.7-dev" + } + } +} diff --git a/guzzle/guzzle/src/Guzzle/Plugin/Md5/CommandContentMd5Plugin.php b/guzzle/guzzle/src/Guzzle/Plugin/Md5/CommandContentMd5Plugin.php new file mode 100644 index 000000000..851242433 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Plugin/Md5/CommandContentMd5Plugin.php @@ -0,0 +1,57 @@ +contentMd5Param = $contentMd5Param; + $this->validateMd5Param = $validateMd5Param; + } + + public static function getSubscribedEvents() + { + return array('command.before_send' => array('onCommandBeforeSend', -255)); + } + + public function onCommandBeforeSend(Event $event) + { + $command = $event['command']; + $request = $command->getRequest(); + + // Only add an MD5 is there is a MD5 option on the operation and it has a payload + if ($request instanceof EntityEnclosingRequestInterface && $request->getBody() + && $command->getOperation()->hasParam($this->contentMd5Param)) { + // Check if an MD5 checksum value should be passed along to the request + if ($command[$this->contentMd5Param] === true) { + if (false !== ($md5 = $request->getBody()->getContentMd5(true, true))) { + $request->setHeader('Content-MD5', $md5); + } + } + } + + // Check if MD5 validation should be used with the response + if ($command[$this->validateMd5Param] === true) { + $request->addSubscriber(new Md5ValidatorPlugin(true, false)); + } + } +} diff --git a/guzzle/guzzle/src/Guzzle/Plugin/Md5/Md5ValidatorPlugin.php b/guzzle/guzzle/src/Guzzle/Plugin/Md5/Md5ValidatorPlugin.php new file mode 100644 index 000000000..5d7a3785e --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Plugin/Md5/Md5ValidatorPlugin.php @@ -0,0 +1,88 @@ +contentLengthCutoff = $contentLengthCutoff; + $this->contentEncoded = $contentEncoded; + } + + public static function getSubscribedEvents() + { + return array('request.complete' => array('onRequestComplete', 255)); + } + + /** + * {@inheritdoc} + * @throws UnexpectedValueException + */ + public function onRequestComplete(Event $event) + { + $response = $event['response']; + + if (!$contentMd5 = $response->getContentMd5()) { + return; + } + + $contentEncoding = $response->getContentEncoding(); + if ($contentEncoding && !$this->contentEncoded) { + return false; + } + + // Make sure that the size of the request is under the cutoff size + if ($this->contentLengthCutoff) { + $size = $response->getContentLength() ?: $response->getBody()->getSize(); + if (!$size || $size > $this->contentLengthCutoff) { + return; + } + } + + if (!$contentEncoding) { + $hash = $response->getBody()->getContentMd5(); + } elseif ($contentEncoding == 'gzip') { + $response->getBody()->compress('zlib.deflate'); + $hash = $response->getBody()->getContentMd5(); + $response->getBody()->uncompress(); + } elseif ($contentEncoding == 'compress') { + $response->getBody()->compress('bzip2.compress'); + $hash = $response->getBody()->getContentMd5(); + $response->getBody()->uncompress(); + } else { + return; + } + + if ($contentMd5 !== $hash) { + throw new UnexpectedValueException( + "The response entity body may have been modified over the wire. The Content-MD5 " + . "received ({$contentMd5}) did not match the calculated MD5 hash ({$hash})." + ); + } + } +} diff --git a/guzzle/guzzle/src/Guzzle/Plugin/Md5/composer.json b/guzzle/guzzle/src/Guzzle/Plugin/Md5/composer.json new file mode 100644 index 000000000..0602d0609 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Plugin/Md5/composer.json @@ -0,0 +1,27 @@ +{ + "name": "guzzle/plugin-md5", + "description": "Guzzle MD5 plugins", + "homepage": "http://guzzlephp.org/", + "keywords": ["plugin", "guzzle"], + "license": "MIT", + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "require": { + "php": ">=5.3.2", + "guzzle/http": "self.version" + }, + "autoload": { + "psr-0": { "Guzzle\\Plugin\\Md5": "" } + }, + "target-dir": "Guzzle/Plugin/Md5", + "extra": { + "branch-alias": { + "dev-master": "3.7-dev" + } + } +} diff --git a/guzzle/guzzle/src/Guzzle/Plugin/Mock/MockPlugin.php b/guzzle/guzzle/src/Guzzle/Plugin/Mock/MockPlugin.php new file mode 100644 index 000000000..ab7833cee --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Plugin/Mock/MockPlugin.php @@ -0,0 +1,242 @@ +readBodies = $readBodies; + $this->temporary = $temporary; + if ($items) { + foreach ($items as $item) { + if ($item instanceof \Exception) { + $this->addException($item); + } else { + $this->addResponse($item); + } + } + } + } + + public static function getSubscribedEvents() + { + // Use a number lower than the CachePlugin + return array('request.before_send' => array('onRequestBeforeSend', -999)); + } + + public static function getAllEvents() + { + return array('mock.request'); + } + + /** + * Get a mock response from a file + * + * @param string $path File to retrieve a mock response from + * + * @return Response + * @throws InvalidArgumentException if the file is not found + */ + public static function getMockFile($path) + { + if (!file_exists($path)) { + throw new InvalidArgumentException('Unable to open mock file: ' . $path); + } + + return Response::fromMessage(file_get_contents($path)); + } + + /** + * Set whether or not to consume the entity body of a request when a mock + * response is used + * + * @param bool $readBodies Set to true to read and consume entity bodies + * + * @return self + */ + public function readBodies($readBodies) + { + $this->readBodies = $readBodies; + + return $this; + } + + /** + * Returns the number of remaining mock responses + * + * @return int + */ + public function count() + { + return count($this->queue); + } + + /** + * Add a response to the end of the queue + * + * @param string|Response $response Response object or path to response file + * + * @return MockPlugin + * @throws InvalidArgumentException if a string or Response is not passed + */ + public function addResponse($response) + { + if (!($response instanceof Response)) { + if (!is_string($response)) { + throw new InvalidArgumentException('Invalid response'); + } + $response = self::getMockFile($response); + } + + $this->queue[] = $response; + + return $this; + } + + /** + * Add an exception to the end of the queue + * + * @param CurlException $e Exception to throw when the request is executed + * + * @return MockPlugin + */ + public function addException(CurlException $e) + { + $this->queue[] = $e; + + return $this; + } + + /** + * Clear the queue + * + * @return MockPlugin + */ + public function clearQueue() + { + $this->queue = array(); + + return $this; + } + + /** + * Returns an array of mock responses remaining in the queue + * + * @return array + */ + public function getQueue() + { + return $this->queue; + } + + /** + * Check if this is a temporary plugin + * + * @return bool + */ + public function isTemporary() + { + return $this->temporary; + } + + /** + * Get a response from the front of the list and add it to a request + * + * @param RequestInterface $request Request to mock + * + * @return self + * @throws CurlException When request.send is called and an exception is queued + */ + public function dequeue(RequestInterface $request) + { + $this->dispatch('mock.request', array('plugin' => $this, 'request' => $request)); + + $item = array_shift($this->queue); + if ($item instanceof Response) { + if ($this->readBodies && $request instanceof EntityEnclosingRequestInterface) { + $request->getEventDispatcher()->addListener('request.sent', $f = function (Event $event) use (&$f) { + while ($data = $event['request']->getBody()->read(8096)); + // Remove the listener after one-time use + $event['request']->getEventDispatcher()->removeListener('request.sent', $f); + }); + } + $request->setResponse($item); + } elseif ($item instanceof CurlException) { + // Emulates exceptions encountered while transferring requests + $item->setRequest($request); + $state = $request->setState(RequestInterface::STATE_ERROR, array('exception' => $item)); + // Only throw if the exception wasn't handled + if ($state == RequestInterface::STATE_ERROR) { + throw $item; + } + } + + return $this; + } + + /** + * Clear the array of received requests + */ + public function flush() + { + $this->received = array(); + } + + /** + * Get an array of requests that were mocked by this plugin + * + * @return array + */ + public function getReceivedRequests() + { + return $this->received; + } + + /** + * Called when a request is about to be sent + * + * @param Event $event + */ + public function onRequestBeforeSend(Event $event) + { + if ($this->queue) { + $request = $event['request']; + $this->received[] = $request; + // Detach the filter from the client so it's a one-time use + if ($this->temporary && count($this->queue) == 1 && $request->getClient()) { + $request->getClient()->getEventDispatcher()->removeSubscriber($this); + } + $this->dequeue($request); + } + } +} diff --git a/guzzle/guzzle/src/Guzzle/Plugin/Mock/composer.json b/guzzle/guzzle/src/Guzzle/Plugin/Mock/composer.json new file mode 100644 index 000000000..f8201e31f --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Plugin/Mock/composer.json @@ -0,0 +1,27 @@ +{ + "name": "guzzle/plugin-mock", + "description": "Guzzle Mock plugin", + "homepage": "http://guzzlephp.org/", + "keywords": ["mock", "plugin", "guzzle"], + "license": "MIT", + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "require": { + "php": ">=5.3.2", + "guzzle/http": "self.version" + }, + "autoload": { + "psr-0": { "Guzzle\\Plugin\\Mock": "" } + }, + "target-dir": "Guzzle/Plugin/Mock", + "extra": { + "branch-alias": { + "dev-master": "3.7-dev" + } + } +} diff --git a/guzzle/guzzle/src/Guzzle/Plugin/Oauth/OauthPlugin.php b/guzzle/guzzle/src/Guzzle/Plugin/Oauth/OauthPlugin.php new file mode 100644 index 000000000..1e1547f65 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Plugin/Oauth/OauthPlugin.php @@ -0,0 +1,305 @@ +config = Collection::fromConfig($config, array( + 'version' => '1.0', + 'request_method' => self::REQUEST_METHOD_HEADER, + 'consumer_key' => 'anonymous', + 'consumer_secret' => 'anonymous', + 'signature_method' => 'HMAC-SHA1', + 'signature_callback' => function($stringToSign, $key) { + return hash_hmac('sha1', $stringToSign, $key, true); + } + ), array( + 'signature_method', 'signature_callback', 'version', + 'consumer_key', 'consumer_secret' + )); + } + + public static function getSubscribedEvents() + { + return array( + 'request.before_send' => array('onRequestBeforeSend', -1000) + ); + } + + /** + * Request before-send event handler + * + * @param Event $event Event received + * @return array + * @throws \InvalidArgumentException + */ + public function onRequestBeforeSend(Event $event) + { + $timestamp = $this->getTimestamp($event); + $request = $event['request']; + $nonce = $this->generateNonce($request); + $authorizationParams = $this->getOauthParams($timestamp, $nonce); + $authorizationParams['oauth_signature'] = $this->getSignature($request, $timestamp, $nonce); + + switch ($this->config['request_method']) { + case self::REQUEST_METHOD_HEADER: + $request->setHeader( + 'Authorization', + $this->buildAuthorizationHeader($authorizationParams) + ); + break; + case self::REQUEST_METHOD_QUERY: + foreach ($authorizationParams as $key => $value) { + $request->getQuery()->set($key, $value); + } + break; + default: + throw new \InvalidArgumentException(sprintf( + 'Invalid consumer method "%s"', + $this->config['request_method'] + )); + } + + return $authorizationParams; + } + + /** + * Builds the Authorization header for a request + * + * @param array $authorizationParams Associative array of authorization parameters + * + * @return string + */ + private function buildAuthorizationHeader($authorizationParams) + { + $authorizationString = 'OAuth '; + foreach ($authorizationParams as $key => $val) { + if ($val) { + $authorizationString .= $key . '="' . urlencode($val) . '", '; + } + } + + return substr($authorizationString, 0, -2); + } + + /** + * Calculate signature for request + * + * @param RequestInterface $request Request to generate a signature for + * @param integer $timestamp Timestamp to use for nonce + * @param string $nonce + * + * @return string + */ + public function getSignature(RequestInterface $request, $timestamp, $nonce) + { + $string = $this->getStringToSign($request, $timestamp, $nonce); + $key = urlencode($this->config['consumer_secret']) . '&' . urlencode($this->config['token_secret']); + + return base64_encode(call_user_func($this->config['signature_callback'], $string, $key)); + } + + /** + * Calculate string to sign + * + * @param RequestInterface $request Request to generate a signature for + * @param int $timestamp Timestamp to use for nonce + * @param string $nonce + * + * @return string + */ + public function getStringToSign(RequestInterface $request, $timestamp, $nonce) + { + $params = $this->getParamsToSign($request, $timestamp, $nonce); + + // Convert booleans to strings. + $params = $this->prepareParameters($params); + + // Build signing string from combined params + $parameterString = new QueryString($params); + + $url = Url::factory($request->getUrl())->setQuery('')->setFragment(null); + + return strtoupper($request->getMethod()) . '&' + . rawurlencode($url) . '&' + . rawurlencode((string) $parameterString); + } + + /** + * Get the oauth parameters as named by the oauth spec + * + * @param $timestamp + * @param $nonce + * @return Collection + */ + protected function getOauthParams($timestamp, $nonce) + { + $params = new Collection(array( + 'oauth_consumer_key' => $this->config['consumer_key'], + 'oauth_nonce' => $nonce, + 'oauth_signature_method' => $this->config['signature_method'], + 'oauth_timestamp' => $timestamp, + )); + + // Optional parameters should not be set if they have not been set in the config as + // the parameter may be considered invalid by the Oauth service. + $optionalParams = array( + 'callback' => 'oauth_callback', + 'token' => 'oauth_token', + 'verifier' => 'oauth_verifier', + 'version' => 'oauth_version' + ); + + foreach ($optionalParams as $optionName => $oauthName) { + if (isset($this->config[$optionName]) == true) { + $params[$oauthName] = $this->config[$optionName]; + } + } + + return $params; + } + + /** + * Get all of the parameters required to sign a request including: + * * The oauth params + * * The request GET params + * * The params passed in the POST body (with a content-type of application/x-www-form-urlencoded) + * + * @param RequestInterface $request Request to generate a signature for + * @param integer $timestamp Timestamp to use for nonce + * @param string $nonce + * + * @return array + */ + public function getParamsToSign(RequestInterface $request, $timestamp, $nonce) + { + $params = $this->getOauthParams($timestamp, $nonce); + + // Add query string parameters + $params->merge($request->getQuery()); + + // Add POST fields to signing string if required + if ($this->shouldPostFieldsBeSigned($request)) + { + $params->merge($request->getPostFields()); + } + + // Sort params + $params = $params->toArray(); + ksort($params); + + return $params; + } + + /** + * Decide whether the post fields should be added to the base string that Oauth signs. + * This implementation is correct. Non-conformant APIs may require that this method be + * overwritten e.g. the Flickr API incorrectly adds the post fields when the Content-Type + * is 'application/x-www-form-urlencoded' + * + * @param $request + * @return bool Whether the post fields should be signed or not + */ + public function shouldPostFieldsBeSigned($request) + { + if (!$this->config->get('disable_post_params') && + $request instanceof EntityEnclosingRequestInterface && + false !== strpos($request->getHeader('Content-Type'), 'application/x-www-form-urlencoded')) + { + return true; + } + + return false; + } + + /** + * Returns a Nonce Based on the unique id and URL. This will allow for multiple requests in parallel with the same + * exact timestamp to use separate nonce's. + * + * @param RequestInterface $request Request to generate a nonce for + * + * @return string + */ + public function generateNonce(RequestInterface $request) + { + return sha1(uniqid('', true) . $request->getUrl()); + } + + /** + * Gets timestamp from event or create new timestamp + * + * @param Event $event Event containing contextual information + * + * @return int + */ + public function getTimestamp(Event $event) + { + return $event['timestamp'] ?: time(); + } + + /** + * Convert booleans to strings, removed unset parameters, and sorts the array + * + * @param array $data Data array + * + * @return array + */ + protected function prepareParameters($data) + { + ksort($data); + foreach ($data as $key => &$value) { + switch (gettype($value)) { + case 'NULL': + unset($data[$key]); + break; + case 'array': + $data[$key] = self::prepareParameters($value); + break; + case 'boolean': + $data[$key] = $value ? 'true' : 'false'; + break; + } + } + + return $data; + } +} diff --git a/guzzle/guzzle/src/Guzzle/Plugin/Oauth/composer.json b/guzzle/guzzle/src/Guzzle/Plugin/Oauth/composer.json new file mode 100644 index 000000000..c9766ba16 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Plugin/Oauth/composer.json @@ -0,0 +1,27 @@ +{ + "name": "guzzle/plugin-oauth", + "description": "Guzzle OAuth plugin", + "homepage": "http://guzzlephp.org/", + "keywords": ["oauth", "plugin", "guzzle"], + "license": "MIT", + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "require": { + "php": ">=5.3.2", + "guzzle/http": "self.version" + }, + "autoload": { + "psr-0": { "Guzzle\\Plugin\\Oauth": "" } + }, + "target-dir": "Guzzle/Plugin/Oauth", + "extra": { + "branch-alias": { + "dev-master": "3.7-dev" + } + } +} diff --git a/guzzle/guzzle/src/Guzzle/Plugin/composer.json b/guzzle/guzzle/src/Guzzle/Plugin/composer.json new file mode 100644 index 000000000..2bbe64cc5 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Plugin/composer.json @@ -0,0 +1,44 @@ +{ + "name": "guzzle/plugin", + "description": "Guzzle plugin component containing all Guzzle HTTP plugins", + "homepage": "http://guzzlephp.org/", + "keywords": ["http", "client", "plugin", "extension", "guzzle"], + "license": "MIT", + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "require": { + "php": ">=5.3.2", + "guzzle/http": "self.version" + }, + "suggest": { + "guzzle/cache": "self.version", + "guzzle/log": "self.version" + }, + "autoload": { + "psr-0": { "Guzzle\\Plugin": "" } + }, + "target-dir": "Guzzle/Plugin", + "replace": { + "guzzle/plugin-async": "self.version", + "guzzle/plugin-backoff": "self.version", + "guzzle/plugin-cache": "self.version", + "guzzle/plugin-cookie": "self.version", + "guzzle/plugin-curlauth": "self.version", + "guzzle/plugin-error-response": "self.version", + "guzzle/plugin-history": "self.version", + "guzzle/plugin-log": "self.version", + "guzzle/plugin-md5": "self.version", + "guzzle/plugin-mock": "self.version", + "guzzle/plugin-oauth": "self.version" + }, + "extra": { + "branch-alias": { + "dev-master": "3.7-dev" + } + } +} diff --git a/guzzle/guzzle/src/Guzzle/Service/AbstractConfigLoader.php b/guzzle/guzzle/src/Guzzle/Service/AbstractConfigLoader.php new file mode 100644 index 000000000..cd06f5722 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Service/AbstractConfigLoader.php @@ -0,0 +1,177 @@ + 'JSON_ERROR_NONE - No errors', + JSON_ERROR_DEPTH => 'JSON_ERROR_DEPTH - Maximum stack depth exceeded', + JSON_ERROR_STATE_MISMATCH => 'JSON_ERROR_STATE_MISMATCH - Underflow or the modes mismatch', + JSON_ERROR_CTRL_CHAR => 'JSON_ERROR_CTRL_CHAR - Unexpected control character found', + JSON_ERROR_SYNTAX => 'JSON_ERROR_SYNTAX - Syntax error, malformed JSON', + JSON_ERROR_UTF8 => 'JSON_ERROR_UTF8 - Malformed UTF-8 characters, possibly incorrectly encoded' + ); + + public function load($config, array $options = array()) + { + // Reset the array of loaded files because this is a new config + $this->loadedFiles = array(); + + if (is_string($config)) { + $config = $this->loadFile($config); + } elseif (!is_array($config)) { + throw new InvalidArgumentException('Unknown type passed to configuration loader: ' . gettype($config)); + } else { + $this->mergeIncludes($config); + } + + return $this->build($config, $options); + } + + /** + * Add an include alias to the loader + * + * @param string $filename Filename to alias (e.g. _foo) + * @param string $alias Actual file to use (e.g. /path/to/foo.json) + * + * @return self + */ + public function addAlias($filename, $alias) + { + $this->aliases[$filename] = $alias; + + return $this; + } + + /** + * Remove an alias from the loader + * + * @param string $alias Alias to remove + * + * @return self + */ + public function removeAlias($alias) + { + unset($this->aliases[$alias]); + + return $this; + } + + /** + * Perform the parsing of a config file and create the end result + * + * @param array $config Configuration data + * @param array $options Options to use when building + * + * @return mixed + */ + protected abstract function build($config, array $options); + + /** + * Load a configuration file (can load JSON or PHP files that return an array when included) + * + * @param string $filename File to load + * + * @return array + * @throws InvalidArgumentException + * @throws RuntimeException when the JSON cannot be parsed + */ + protected function loadFile($filename) + { + if (isset($this->aliases[$filename])) { + $filename = $this->aliases[$filename]; + } + + switch (pathinfo($filename, PATHINFO_EXTENSION)) { + case 'js': + case 'json': + $level = error_reporting(0); + $json = file_get_contents($filename); + error_reporting($level); + + if ($json === false) { + $err = error_get_last(); + throw new InvalidArgumentException("Unable to open {$filename}: " . $err['message']); + } + + $config = json_decode($json, true); + // Throw an exception if there was an error loading the file + if ($error = json_last_error()) { + $message = isset(self::$jsonErrors[$error]) ? self::$jsonErrors[$error] : 'Unknown error'; + throw new RuntimeException("Error loading JSON data from {$filename}: ({$error}) - {$message}"); + } + break; + case 'php': + if (!is_readable($filename)) { + throw new InvalidArgumentException("Unable to open {$filename} for reading"); + } + $config = require $filename; + if (!is_array($config)) { + throw new InvalidArgumentException('PHP files must return an array of configuration data'); + } + break; + default: + throw new InvalidArgumentException('Unknown file extension: ' . $filename); + } + + // Keep track of this file being loaded to prevent infinite recursion + $this->loadedFiles[$filename] = true; + + // Merge include files into the configuration array + $this->mergeIncludes($config, dirname($filename)); + + return $config; + } + + /** + * Merges in all include files + * + * @param array $config Config data that contains includes + * @param string $basePath Base path to use when a relative path is encountered + * + * @return array Returns the merged and included data + */ + protected function mergeIncludes(&$config, $basePath = null) + { + if (!empty($config['includes'])) { + foreach ($config['includes'] as &$path) { + // Account for relative paths + if ($path[0] != DIRECTORY_SEPARATOR && !isset($this->aliases[$path]) && $basePath) { + $path = "{$basePath}/{$path}"; + } + // Don't load the same files more than once + if (!isset($this->loadedFiles[$path])) { + $this->loadedFiles[$path] = true; + $config = $this->mergeData($this->loadFile($path), $config); + } + } + } + } + + /** + * Default implementation for merging two arrays of data (uses array_merge_recursive) + * + * @param array $a Original data + * @param array $b Data to merge into the original and overwrite existing values + * + * @return array + */ + protected function mergeData(array $a, array $b) + { + return array_merge_recursive($a, $b); + } +} diff --git a/guzzle/guzzle/src/Guzzle/Service/Builder/ServiceBuilder.php b/guzzle/guzzle/src/Guzzle/Service/Builder/ServiceBuilder.php new file mode 100644 index 000000000..38150db4b --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Service/Builder/ServiceBuilder.php @@ -0,0 +1,189 @@ +load($config, $globalParameters); + } + + /** + * @param array $serviceBuilderConfig Service configuration settings: + * - name: Name of the service + * - class: Client class to instantiate using a factory method + * - params: array of key value pair configuration settings for the builder + */ + public function __construct(array $serviceBuilderConfig = array()) + { + $this->builderConfig = $serviceBuilderConfig; + } + + public static function getAllEvents() + { + return array('service_builder.create_client'); + } + + public function unserialize($serialized) + { + $this->builderConfig = json_decode($serialized, true); + } + + public function serialize() + { + return json_encode($this->builderConfig); + } + + /** + * Attach a plugin to every client created by the builder + * + * @param EventSubscriberInterface $plugin Plugin to attach to each client + * + * @return self + */ + public function addGlobalPlugin(EventSubscriberInterface $plugin) + { + $this->plugins[] = $plugin; + + return $this; + } + + /** + * Get data from the service builder without triggering the building of a service + * + * @param string $name Name of the service to retrieve + * + * @return array|null + */ + public function getData($name) + { + return isset($this->builderConfig[$name]) ? $this->builderConfig[$name] : null; + } + + public function get($name, $throwAway = false) + { + if (!isset($this->builderConfig[$name])) { + + // Check to see if arbitrary data is being referenced + if (isset($this->clients[$name])) { + return $this->clients[$name]; + } + + // Check aliases and return a match if found + foreach ($this->builderConfig as $actualName => $config) { + if (isset($config['alias']) && $config['alias'] == $name) { + return $this->get($actualName, $throwAway); + } + } + throw new ServiceNotFoundException('No service is registered as ' . $name); + } + + if (!$throwAway && isset($this->clients[$name])) { + return $this->clients[$name]; + } + + $builder =& $this->builderConfig[$name]; + + // Convert references to the actual client + foreach ($builder['params'] as &$v) { + if (is_string($v) && substr($v, 0, 1) == '{' && substr($v, -1) == '}') { + $v = $this->get(trim($v, '{} ')); + } + } + + // Get the configured parameters and merge in any parameters provided for throw-away clients + $config = $builder['params']; + if (is_array($throwAway)) { + $config = $throwAway + $config; + } + + $client = $builder['class']::factory($config); + + if (!$throwAway) { + $this->clients[$name] = $client; + } + + if ($client instanceof ClientInterface) { + foreach ($this->plugins as $plugin) { + $client->addSubscriber($plugin); + } + // Dispatch an event letting listeners know a client was created + $this->dispatch('service_builder.create_client', array('client' => $client)); + } + + return $client; + } + + public function set($key, $service) + { + if (is_array($service) && isset($service['class']) && isset($service['params'])) { + $this->builderConfig[$key] = $service; + } else { + $this->clients[$key] = $service; + } + + return $this; + } + + public function offsetSet($offset, $value) + { + $this->set($offset, $value); + } + + public function offsetUnset($offset) + { + unset($this->builderConfig[$offset]); + unset($this->clients[$offset]); + } + + public function offsetExists($offset) + { + return isset($this->builderConfig[$offset]) || isset($this->clients[$offset]); + } + + public function offsetGet($offset) + { + return $this->get($offset); + } +} diff --git a/guzzle/guzzle/src/Guzzle/Service/Builder/ServiceBuilderInterface.php b/guzzle/guzzle/src/Guzzle/Service/Builder/ServiceBuilderInterface.php new file mode 100644 index 000000000..4fc310a47 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Service/Builder/ServiceBuilderInterface.php @@ -0,0 +1,40 @@ + &$service) { + + $service['params'] = isset($service['params']) ? $service['params'] : array(); + + // Check if this client builder extends another client + if (!empty($service['extends'])) { + + // Make sure that the service it's extending has been defined + if (!isset($services[$service['extends']])) { + throw new ServiceNotFoundException( + "{$name} is trying to extend a non-existent service: {$service['extends']}" + ); + } + + $extended = &$services[$service['extends']]; + + // Use the correct class attribute + if (empty($service['class'])) { + $service['class'] = isset($extended['class']) ? $extended['class'] : ''; + } + if ($extendsParams = isset($extended['params']) ? $extended['params'] : false) { + $service['params'] = $service['params'] + $extendsParams; + } + } + + // Overwrite default values with global parameter values + if (!empty($options)) { + $service['params'] = $options + $service['params']; + } + + $service['class'] = isset($service['class']) ? $service['class'] : ''; + } + + return new $class($services); + } + + protected function mergeData(array $a, array $b) + { + $result = $b + $a; + + // Merge services using a recursive union of arrays + if (isset($a['services']) && $b['services']) { + + // Get a union of the services of the two arrays + $result['services'] = $b['services'] + $a['services']; + + // Merge each service in using a union of the two arrays + foreach ($result['services'] as $name => &$service) { + + // By default, services completely override a previously defined service unless it extends itself + if (isset($a['services'][$name]['extends']) + && isset($b['services'][$name]['extends']) + && $b['services'][$name]['extends'] == $name + ) { + $service += $a['services'][$name]; + // Use the `extends` attribute of the parent + $service['extends'] = $a['services'][$name]['extends']; + // Merge parameters using a union if both have parameters + if (isset($a['services'][$name]['params'])) { + $service['params'] += $a['services'][$name]['params']; + } + } + } + } + + return $result; + } +} diff --git a/guzzle/guzzle/src/Guzzle/Service/CachingConfigLoader.php b/guzzle/guzzle/src/Guzzle/Service/CachingConfigLoader.php new file mode 100644 index 000000000..26f8360cc --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Service/CachingConfigLoader.php @@ -0,0 +1,46 @@ +loader = $loader; + $this->cache = $cache; + } + + public function load($config, array $options = array()) + { + if (!is_string($config)) { + $key = false; + } else { + $key = 'loader_' . crc32($config); + if ($result = $this->cache->fetch($key)) { + return $result; + } + } + + $result = $this->loader->load($config, $options); + if ($key) { + $this->cache->save($key, $result); + } + + return $result; + } +} diff --git a/guzzle/guzzle/src/Guzzle/Service/Client.php b/guzzle/guzzle/src/Guzzle/Service/Client.php new file mode 100644 index 000000000..f4b76be1c --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Service/Client.php @@ -0,0 +1,293 @@ +getCommand($method, isset($args[0]) ? $args[0] : array())->getResult(); + } + + public function getCommand($name, array $args = array()) + { + // Add global client options to the command + if ($options = $this->getConfig(self::COMMAND_PARAMS)) { + $args += $options; + } + + if (!($command = $this->getCommandFactory()->factory($name, $args))) { + throw new InvalidArgumentException("Command was not found matching {$name}"); + } + + $command->setClient($this); + $this->dispatch('client.command.create', array('client' => $this, 'command' => $command)); + + return $command; + } + + /** + * Set the command factory used to create commands by name + * + * @param CommandFactoryInterface $factory Command factory + * + * @return self + */ + public function setCommandFactory(CommandFactoryInterface $factory) + { + $this->commandFactory = $factory; + + return $this; + } + + /** + * Set the resource iterator factory associated with the client + * + * @param ResourceIteratorFactoryInterface $factory Resource iterator factory + * + * @return self + */ + public function setResourceIteratorFactory(ResourceIteratorFactoryInterface $factory) + { + $this->resourceIteratorFactory = $factory; + + return $this; + } + + public function getIterator($command, array $commandOptions = null, array $iteratorOptions = array()) + { + if (!($command instanceof CommandInterface)) { + $command = $this->getCommand($command, $commandOptions ?: array()); + } + + return $this->getResourceIteratorFactory()->build($command, $iteratorOptions); + } + + public function execute($command) + { + if ($command instanceof CommandInterface) { + $this->send($this->prepareCommand($command)); + $this->dispatch('command.after_send', array('command' => $command)); + return $command->getResult(); + } elseif (is_array($command) || $command instanceof \Traversable) { + return $this->executeMultiple($command); + } else { + throw new InvalidArgumentException('Command must be a command or array of commands'); + } + } + + public function setDescription(ServiceDescriptionInterface $service) + { + $this->serviceDescription = $service; + + // If a baseUrl was set on the description, then update the client + if ($baseUrl = $service->getBaseUrl()) { + $this->setBaseUrl($baseUrl); + } + + return $this; + } + + public function getDescription() + { + return $this->serviceDescription; + } + + /** + * Set the inflector used with the client + * + * @param InflectorInterface $inflector Inflection object + * + * @return self + */ + public function setInflector(InflectorInterface $inflector) + { + $this->inflector = $inflector; + + return $this; + } + + /** + * Get the inflector used with the client + * + * @return self + */ + public function getInflector() + { + if (!$this->inflector) { + $this->inflector = Inflector::getDefault(); + } + + return $this->inflector; + } + + /** + * Prepare a command for sending and get the RequestInterface object created by the command + * + * @param CommandInterface $command Command to prepare + * + * @return RequestInterface + */ + protected function prepareCommand(CommandInterface $command) + { + // Set the client and prepare the command + $request = $command->setClient($this)->prepare(); + // Set the state to new if the command was previously executed + $request->setState(RequestInterface::STATE_NEW); + $this->dispatch('command.before_send', array('command' => $command)); + + return $request; + } + + /** + * Execute multiple commands in parallel + * + * @param array|Traversable $commands Array of CommandInterface objects to execute + * + * @return array Returns an array of the executed commands + * @throws Exception\CommandTransferException + */ + protected function executeMultiple($commands) + { + $requests = array(); + $commandRequests = new \SplObjectStorage(); + + foreach ($commands as $command) { + $request = $this->prepareCommand($command); + $commandRequests[$request] = $command; + $requests[] = $request; + } + + try { + $this->send($requests); + foreach ($commands as $command) { + $this->dispatch('command.after_send', array('command' => $command)); + } + return $commands; + } catch (MultiTransferException $failureException) { + // Throw a CommandTransferException using the successful and failed commands + $e = CommandTransferException::fromMultiTransferException($failureException); + + // Remove failed requests from the successful requests array and add to the failures array + foreach ($failureException->getFailedRequests() as $request) { + if (isset($commandRequests[$request])) { + $e->addFailedCommand($commandRequests[$request]); + unset($commandRequests[$request]); + } + } + + // Always emit the command after_send events for successful commands + foreach ($commandRequests as $success) { + $e->addSuccessfulCommand($commandRequests[$success]); + $this->dispatch('command.after_send', array('command' => $commandRequests[$success])); + } + + throw $e; + } + } + + protected function getResourceIteratorFactory() + { + if (!$this->resourceIteratorFactory) { + // Build the default resource iterator factory if one is not set + $clientClass = get_class($this); + $prefix = substr($clientClass, 0, strrpos($clientClass, '\\')); + $this->resourceIteratorFactory = new ResourceIteratorClassFactory(array( + "{$prefix}\\Iterator", + "{$prefix}\\Model" + )); + } + + return $this->resourceIteratorFactory; + } + + /** + * Get the command factory associated with the client + * + * @return CommandFactoryInterface + */ + protected function getCommandFactory() + { + if (!$this->commandFactory) { + $this->commandFactory = CompositeFactory::getDefaultChain($this); + } + + return $this->commandFactory; + } + + /** + * @deprecated + * @codeCoverageIgnore + */ + public function enableMagicMethods($isEnabled) + { + Version::warn(__METHOD__ . ' is deprecated'); + } +} diff --git a/guzzle/guzzle/src/Guzzle/Service/ClientInterface.php b/guzzle/guzzle/src/Guzzle/Service/ClientInterface.php new file mode 100644 index 000000000..814154f00 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Service/ClientInterface.php @@ -0,0 +1,68 @@ +operation = $operation ?: $this->createOperation(); + foreach ($this->operation->getParams() as $name => $arg) { + $currentValue = $this[$name]; + $configValue = $arg->getValue($currentValue); + // If default or static values are set, then this should always be updated on the config object + if ($currentValue !== $configValue) { + $this[$name] = $configValue; + } + } + + $headers = $this[self::HEADERS_OPTION]; + if (!$headers instanceof Collection) { + $this[self::HEADERS_OPTION] = new Collection((array) $headers); + } + + // You can set a command.on_complete option in your parameters to set an onComplete callback + if ($onComplete = $this['command.on_complete']) { + unset($this['command.on_complete']); + $this->setOnComplete($onComplete); + } + + // Set the hidden additional parameters + if (!$this[self::HIDDEN_PARAMS]) { + $this[self::HIDDEN_PARAMS] = array( + self::HEADERS_OPTION, + self::RESPONSE_PROCESSING, + self::HIDDEN_PARAMS, + self::REQUEST_OPTIONS + ); + } + + $this->init(); + } + + /** + * Custom clone behavior + */ + public function __clone() + { + $this->request = null; + $this->result = null; + } + + /** + * Execute the command in the same manner as calling a function + * + * @return mixed Returns the result of {@see AbstractCommand::execute} + */ + public function __invoke() + { + return $this->execute(); + } + + public function getName() + { + return $this->operation->getName(); + } + + /** + * Get the API command information about the command + * + * @return OperationInterface + */ + public function getOperation() + { + return $this->operation; + } + + public function setOnComplete($callable) + { + if (!is_callable($callable)) { + throw new InvalidArgumentException('The onComplete function must be callable'); + } + + $this->onComplete = $callable; + + return $this; + } + + public function execute() + { + if (!$this->client) { + throw new CommandException('A client must be associated with the command before it can be executed.'); + } + + return $this->client->execute($this); + } + + public function getClient() + { + return $this->client; + } + + public function setClient(ClientInterface $client) + { + $this->client = $client; + + return $this; + } + + public function getRequest() + { + if (!$this->request) { + throw new CommandException('The command must be prepared before retrieving the request'); + } + + return $this->request; + } + + public function getResponse() + { + if (!$this->isExecuted()) { + $this->execute(); + } + + return $this->request->getResponse(); + } + + public function getResult() + { + if (!$this->isExecuted()) { + $this->execute(); + } + + if (null === $this->result) { + $this->process(); + // Call the onComplete method if one is set + if ($this->onComplete) { + call_user_func($this->onComplete, $this); + } + } + + return $this->result; + } + + public function setResult($result) + { + $this->result = $result; + + return $this; + } + + public function isPrepared() + { + return $this->request !== null; + } + + public function isExecuted() + { + return $this->request !== null && $this->request->getState() == 'complete'; + } + + public function prepare() + { + if (!$this->isPrepared()) { + if (!$this->client) { + throw new CommandException('A client must be associated with the command before it can be prepared.'); + } + + // If no response processing value was specified, then attempt to use the highest level of processing + if (!isset($this[self::RESPONSE_PROCESSING])) { + $this[self::RESPONSE_PROCESSING] = self::TYPE_MODEL; + } + + // Notify subscribers of the client that the command is being prepared + $this->client->dispatch('command.before_prepare', array('command' => $this)); + + // Fail on missing required arguments, and change parameters via filters + $this->validate(); + // Delegate to the subclass that implements the build method + $this->build(); + + // Add custom request headers set on the command + if ($headers = $this[self::HEADERS_OPTION]) { + foreach ($headers as $key => $value) { + $this->request->setHeader($key, $value); + } + } + + // Add any curl options to the request + if ($options = $this[Client::CURL_OPTIONS]) { + $this->request->getCurlOptions()->overwriteWith(CurlHandle::parseCurlConfig($options)); + } + + // Set a custom response body + if ($responseBody = $this[self::RESPONSE_BODY]) { + $this->request->setResponseBody($responseBody); + } + + $this->client->dispatch('command.after_prepare', array('command' => $this)); + } + + return $this->request; + } + + /** + * Set the validator used to validate and prepare command parameters and nested JSON schemas. If no validator is + * set, then the command will validate using the default {@see SchemaValidator}. + * + * @param ValidatorInterface $validator Validator used to prepare and validate properties against a JSON schema + * + * @return self + */ + public function setValidator(ValidatorInterface $validator) + { + $this->validator = $validator; + + return $this; + } + + public function getRequestHeaders() + { + return $this[self::HEADERS_OPTION]; + } + + /** + * Initialize the command (hook that can be implemented in subclasses) + */ + protected function init() {} + + /** + * Create the request object that will carry out the command + */ + abstract protected function build(); + + /** + * Hook used to create an operation for concrete commands that are not associated with a service description + * + * @return OperationInterface + */ + protected function createOperation() + { + return new Operation(array('name' => get_class($this))); + } + + /** + * Create the result of the command after the request has been completed. + * Override this method in subclasses to customize this behavior + */ + protected function process() + { + $this->result = $this[self::RESPONSE_PROCESSING] != self::TYPE_RAW + ? DefaultResponseParser::getInstance()->parse($this) + : $this->request->getResponse(); + } + + /** + * Validate and prepare the command based on the schema and rules defined by the command's Operation object + * + * @throws ValidationException when validation errors occur + */ + protected function validate() + { + // Do not perform request validation/transformation if it is disable + if ($this[self::DISABLE_VALIDATION]) { + return; + } + + $errors = array(); + $validator = $this->getValidator(); + foreach ($this->operation->getParams() as $name => $schema) { + $value = $this[$name]; + if (!$validator->validate($schema, $value)) { + $errors = array_merge($errors, $validator->getErrors()); + } elseif ($value !== $this[$name]) { + // Update the config value if it changed and no validation errors were encountered + $this->data[$name] = $value; + } + } + + // Validate additional parameters + $hidden = $this[self::HIDDEN_PARAMS]; + + if ($properties = $this->operation->getAdditionalParameters()) { + foreach ($this->toArray() as $name => $value) { + // It's only additional if it isn't defined in the schema + if (!$this->operation->hasParam($name) && !in_array($name, $hidden)) { + // Always set the name so that error messages are useful + $properties->setName($name); + if (!$validator->validate($properties, $value)) { + $errors = array_merge($errors, $validator->getErrors()); + } elseif ($value !== $this[$name]) { + $this->data[$name] = $value; + } + } + } + } + + if (!empty($errors)) { + $e = new ValidationException('Validation errors: ' . implode("\n", $errors)); + $e->setErrors($errors); + throw $e; + } + } + + /** + * Get the validator used to prepare and validate properties. If no validator has been set on the command, then + * the default {@see SchemaValidator} will be used. + * + * @return ValidatorInterface + */ + protected function getValidator() + { + if (!$this->validator) { + $this->validator = SchemaValidator::getInstance(); + } + + return $this->validator; + } + + /** + * Get array of any validation errors + * If no validator has been set then return false + */ + public function getValidationErrors() + { + if (!$this->validator) { + return false; + } + + return $this->validator->getErrors(); + } +} diff --git a/guzzle/guzzle/src/Guzzle/Service/Command/ClosureCommand.php b/guzzle/guzzle/src/Guzzle/Service/Command/ClosureCommand.php new file mode 100644 index 000000000..cb6ac40ce --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Service/Command/ClosureCommand.php @@ -0,0 +1,41 @@ +request = $closure($this, $this->operation); + + if (!$this->request || !$this->request instanceof RequestInterface) { + throw new UnexpectedValueException('Closure command did not return a RequestInterface object'); + } + } +} diff --git a/guzzle/guzzle/src/Guzzle/Service/Command/CommandInterface.php b/guzzle/guzzle/src/Guzzle/Service/Command/CommandInterface.php new file mode 100644 index 000000000..fbb61d2ff --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Service/Command/CommandInterface.php @@ -0,0 +1,128 @@ +stopPropagation(); + } + + /** + * Get the created object + * + * @return mixed + */ + public function getResult() + { + return $this['result']; + } +} diff --git a/guzzle/guzzle/src/Guzzle/Service/Command/DefaultRequestSerializer.php b/guzzle/guzzle/src/Guzzle/Service/Command/DefaultRequestSerializer.php new file mode 100644 index 000000000..2dc4acd37 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Service/Command/DefaultRequestSerializer.php @@ -0,0 +1,169 @@ +factory = $factory; + } + + /** + * Add a location visitor to the serializer + * + * @param string $location Location to associate with the visitor + * @param RequestVisitorInterface $visitor Visitor to attach + * + * @return self + */ + public function addVisitor($location, RequestVisitorInterface $visitor) + { + $this->factory->addRequestVisitor($location, $visitor); + + return $this; + } + + public function prepare(CommandInterface $command) + { + $request = $this->createRequest($command); + // Keep an array of visitors found in the operation + $foundVisitors = array(); + $operation = $command->getOperation(); + + // Add arguments to the request using the location attribute + foreach ($operation->getParams() as $name => $arg) { + /** @var $arg \Guzzle\Service\Description\Parameter */ + $location = $arg->getLocation(); + // Skip 'uri' locations because they've already been processed + if ($location && $location != 'uri') { + // Instantiate visitors as they are detected in the properties + if (!isset($foundVisitors[$location])) { + $foundVisitors[$location] = $this->factory->getRequestVisitor($location); + } + // Ensure that a value has been set for this parameter + $value = $command[$name]; + if ($value !== null) { + // Apply the parameter value with the location visitor + $foundVisitors[$location]->visit($command, $request, $arg, $value); + } + } + } + + // Serialize additional parameters + if ($additional = $operation->getAdditionalParameters()) { + if ($visitor = $this->prepareAdditionalParameters($operation, $command, $request, $additional)) { + $foundVisitors[$additional->getLocation()] = $visitor; + } + } + + // Call the after method on each visitor found in the operation + foreach ($foundVisitors as $visitor) { + $visitor->after($command, $request); + } + + return $request; + } + + /** + * Serialize additional parameters + * + * @param OperationInterface $operation Operation that owns the command + * @param CommandInterface $command Command to prepare + * @param RequestInterface $request Request to serialize + * @param Parameter $additional Additional parameters + * + * @return null|RequestVisitorInterface + */ + protected function prepareAdditionalParameters( + OperationInterface $operation, + CommandInterface $command, + RequestInterface $request, + Parameter $additional + ) { + if (!($location = $additional->getLocation())) { + return; + } + + $visitor = $this->factory->getRequestVisitor($location); + $hidden = $command[$command::HIDDEN_PARAMS]; + + foreach ($command->toArray() as $key => $value) { + // Ignore values that are null or built-in command options + if ($value !== null + && !in_array($key, $hidden) + && !$operation->hasParam($key) + ) { + $additional->setName($key); + $visitor->visit($command, $request, $additional, $value); + } + } + + return $visitor; + } + + /** + * Create a request for the command and operation + * + * @param CommandInterface $command Command to create a request for + * + * @return RequestInterface + */ + protected function createRequest(CommandInterface $command) + { + $operation = $command->getOperation(); + $client = $command->getClient(); + $options = $command[AbstractCommand::REQUEST_OPTIONS] ?: array(); + + // If the command does not specify a template, then assume the base URL of the client + if (!($uri = $operation->getUri())) { + return $client->createRequest($operation->getHttpMethod(), $client->getBaseUrl(), null, null, $options); + } + + // Get the path values and use the client config settings + $variables = array(); + foreach ($operation->getParams() as $name => $arg) { + if ($arg->getLocation() == 'uri') { + if (isset($command[$name])) { + $variables[$name] = $arg->filter($command[$name]); + if (!is_array($variables[$name])) { + $variables[$name] = (string) $variables[$name]; + } + } + } + } + + return $client->createRequest($operation->getHttpMethod(), array($uri, $variables), null, null, $options); + } +} diff --git a/guzzle/guzzle/src/Guzzle/Service/Command/DefaultResponseParser.php b/guzzle/guzzle/src/Guzzle/Service/Command/DefaultResponseParser.php new file mode 100644 index 000000000..4fe380376 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Service/Command/DefaultResponseParser.php @@ -0,0 +1,55 @@ +getRequest()->getResponse(); + + // Account for hard coded content-type values specified in service descriptions + if ($contentType = $command['command.expects']) { + $response->setHeader('Content-Type', $contentType); + } else { + $contentType = (string) $response->getHeader('Content-Type'); + } + + return $this->handleParsing($command, $response, $contentType); + } + + protected function handleParsing(CommandInterface $command, Response $response, $contentType) + { + $result = $response; + if ($result->getBody()) { + if (stripos($contentType, 'json') !== false) { + $result = $result->json(); + } elseif (stripos($contentType, 'xml') !== false) { + $result = $result->xml(); + } + } + + return $result; + } +} diff --git a/guzzle/guzzle/src/Guzzle/Service/Command/Factory/AliasFactory.php b/guzzle/guzzle/src/Guzzle/Service/Command/Factory/AliasFactory.php new file mode 100644 index 000000000..1c5ce0741 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Service/Command/Factory/AliasFactory.php @@ -0,0 +1,39 @@ +client = $client; + $this->aliases = $aliases; + } + + public function factory($name, array $args = array()) + { + if (isset($this->aliases[$name])) { + try { + return $this->client->getCommand($this->aliases[$name], $args); + } catch (InvalidArgumentException $e) { + return null; + } + } + } +} diff --git a/guzzle/guzzle/src/Guzzle/Service/Command/Factory/CompositeFactory.php b/guzzle/guzzle/src/Guzzle/Service/Command/Factory/CompositeFactory.php new file mode 100644 index 000000000..8c46983d6 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Service/Command/Factory/CompositeFactory.php @@ -0,0 +1,154 @@ +getDescription()) { + $factories[] = new ServiceDescriptionFactory($description); + } + $factories[] = new ConcreteClassFactory($client); + + return new self($factories); + } + + /** + * @param array $factories Array of command factories + */ + public function __construct(array $factories = array()) + { + $this->factories = $factories; + } + + /** + * Add a command factory to the chain + * + * @param FactoryInterface $factory Factory to add + * @param string|FactoryInterface $before Insert the new command factory before a command factory class or object + * matching a class name. + * @return CompositeFactory + */ + public function add(FactoryInterface $factory, $before = null) + { + $pos = null; + + if ($before) { + foreach ($this->factories as $i => $f) { + if ($before instanceof FactoryInterface) { + if ($f === $before) { + $pos = $i; + break; + } + } elseif (is_string($before)) { + if ($f instanceof $before) { + $pos = $i; + break; + } + } + } + } + + if ($pos === null) { + $this->factories[] = $factory; + } else { + array_splice($this->factories, $i, 0, array($factory)); + } + + return $this; + } + + /** + * Check if the chain contains a specific command factory + * + * @param FactoryInterface|string $factory Factory to check + * + * @return bool + */ + public function has($factory) + { + return (bool) $this->find($factory); + } + + /** + * Remove a specific command factory from the chain + * + * @param string|FactoryInterface $factory Factory to remove by name or instance + * + * @return CompositeFactory + */ + public function remove($factory = null) + { + if (!($factory instanceof FactoryInterface)) { + $factory = $this->find($factory); + } + + $this->factories = array_values(array_filter($this->factories, function($f) use ($factory) { + return $f !== $factory; + })); + + return $this; + } + + /** + * Get a command factory by class name + * + * @param string|FactoryInterface $factory Command factory class or instance + * + * @return null|FactoryInterface + */ + public function find($factory) + { + foreach ($this->factories as $f) { + if ($factory === $f || (is_string($factory) && $f instanceof $factory)) { + return $f; + } + } + } + + /** + * Create a command using the associated command factories + * + * @param string $name Name of the command + * @param array $args Command arguments + * + * @return CommandInterface + */ + public function factory($name, array $args = array()) + { + foreach ($this->factories as $factory) { + $command = $factory->factory($name, $args); + if ($command) { + return $command; + } + } + } + + public function count() + { + return count($this->factories); + } + + public function getIterator() + { + return new \ArrayIterator($this->factories); + } +} diff --git a/guzzle/guzzle/src/Guzzle/Service/Command/Factory/ConcreteClassFactory.php b/guzzle/guzzle/src/Guzzle/Service/Command/Factory/ConcreteClassFactory.php new file mode 100644 index 000000000..0e93deaa0 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Service/Command/Factory/ConcreteClassFactory.php @@ -0,0 +1,47 @@ +client = $client; + $this->inflector = $inflector ?: Inflector::getDefault(); + } + + public function factory($name, array $args = array()) + { + // Determine the class to instantiate based on the namespace of the current client and the default directory + $prefix = $this->client->getConfig('command.prefix'); + if (!$prefix) { + // The prefix can be specified in a factory method and is cached + $prefix = implode('\\', array_slice(explode('\\', get_class($this->client)), 0, -1)) . '\\Command\\'; + $this->client->getConfig()->set('command.prefix', $prefix); + } + + $class = $prefix . str_replace(' ', '\\', ucwords(str_replace('.', ' ', $this->inflector->camel($name)))); + + // Create the concrete command if it exists + if (class_exists($class)) { + return new $class($args); + } + } +} diff --git a/guzzle/guzzle/src/Guzzle/Service/Command/Factory/FactoryInterface.php b/guzzle/guzzle/src/Guzzle/Service/Command/Factory/FactoryInterface.php new file mode 100644 index 000000000..35c299d9d --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Service/Command/Factory/FactoryInterface.php @@ -0,0 +1,21 @@ +map = $map; + } + + public function factory($name, array $args = array()) + { + if (isset($this->map[$name])) { + $class = $this->map[$name]; + + return new $class($args); + } + } +} diff --git a/guzzle/guzzle/src/Guzzle/Service/Command/Factory/ServiceDescriptionFactory.php b/guzzle/guzzle/src/Guzzle/Service/Command/Factory/ServiceDescriptionFactory.php new file mode 100644 index 000000000..b943a5b50 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Service/Command/Factory/ServiceDescriptionFactory.php @@ -0,0 +1,71 @@ +setServiceDescription($description); + $this->inflector = $inflector; + } + + /** + * Change the service description used with the factory + * + * @param ServiceDescriptionInterface $description Service description to use + * + * @return FactoryInterface + */ + public function setServiceDescription(ServiceDescriptionInterface $description) + { + $this->description = $description; + + return $this; + } + + /** + * Returns the service description + * + * @return ServiceDescriptionInterface + */ + public function getServiceDescription() + { + return $this->description; + } + + public function factory($name, array $args = array()) + { + $command = $this->description->getOperation($name); + + // If a command wasn't found, then try to uppercase the first letter and try again + if (!$command) { + $command = $this->description->getOperation(ucfirst($name)); + // If an inflector was passed, then attempt to get the command using snake_case inflection + if (!$command && $this->inflector) { + $command = $this->description->getOperation($this->inflector->snake($name)); + } + } + + if ($command) { + $class = $command->getClass(); + return new $class($args, $command, $this->description); + } + } +} diff --git a/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/AbstractRequestVisitor.php b/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/AbstractRequestVisitor.php new file mode 100644 index 000000000..adcfca1ba --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/AbstractRequestVisitor.php @@ -0,0 +1,69 @@ +resolveRecursively($value, $param) + : $param->filter($value); + } + + /** + * Map nested parameters into the location_key based parameters + * + * @param array $value Value to map + * @param Parameter $param Parameter that holds information about the current key + * + * @return array Returns the mapped array + */ + protected function resolveRecursively(array $value, Parameter $param) + { + foreach ($value as $name => &$v) { + switch ($param->getType()) { + case 'object': + if ($subParam = $param->getProperty($name)) { + $key = $subParam->getWireName(); + $value[$key] = $this->prepareValue($v, $subParam); + if ($name != $key) { + unset($value[$name]); + } + } elseif ($param->getAdditionalProperties() instanceof Parameter) { + $v = $this->prepareValue($v, $param->getAdditionalProperties()); + } + break; + case 'array': + if ($items = $param->getItems()) { + $v = $this->prepareValue($v, $items); + } + break; + } + } + + return $param->filter($value); + } +} diff --git a/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/BodyVisitor.php b/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/BodyVisitor.php new file mode 100644 index 000000000..168d7806f --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/BodyVisitor.php @@ -0,0 +1,58 @@ +filter($value); + $entityBody = EntityBody::factory($value); + $request->setBody($entityBody); + $this->addExpectHeader($request, $entityBody, $param->getData('expect_header')); + // Add the Content-Encoding header if one is set on the EntityBody + if ($encoding = $entityBody->getContentEncoding()) { + $request->setHeader('Content-Encoding', $encoding); + } + } + + /** + * Add the appropriate expect header to a request + * + * @param EntityEnclosingRequestInterface $request Request to update + * @param EntityBodyInterface $body Entity body of the request + * @param string|int $expect Expect header setting + */ + protected function addExpectHeader(EntityEnclosingRequestInterface $request, EntityBodyInterface $body, $expect) + { + // Allow the `expect` data parameter to be set to remove the Expect header from the request + if ($expect === false) { + $request->removeHeader('Expect'); + } elseif ($expect !== true) { + // Default to using a MB as the point in which to start using the expect header + $expect = $expect ?: 1048576; + // If the expect_header value is numeric then only add if the size is greater than the cutoff + if (is_numeric($expect) && $body->getSize()) { + if ($body->getSize() < $expect) { + $request->removeHeader('Expect'); + } else { + $request->setHeader('Expect', '100-Continue'); + } + } + } + } +} diff --git a/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/HeaderVisitor.php b/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/HeaderVisitor.php new file mode 100644 index 000000000..2a537542c --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/HeaderVisitor.php @@ -0,0 +1,44 @@ +filter($value); + if ($param->getType() == 'object' && $param->getAdditionalProperties() instanceof Parameter) { + $this->addPrefixedHeaders($request, $param, $value); + } else { + $request->setHeader($param->getWireName(), $value); + } + } + + /** + * Add a prefixed array of headers to the request + * + * @param RequestInterface $request Request to update + * @param Parameter $param Parameter object + * @param array $value Header array to add + * + * @throws InvalidArgumentException + */ + protected function addPrefixedHeaders(RequestInterface $request, Parameter $param, $value) + { + if (!is_array($value)) { + throw new InvalidArgumentException('An array of mapped headers expected, but received a single value'); + } + $prefix = $param->getSentAs(); + foreach ($value as $headerName => $headerValue) { + $request->setHeader($prefix . $headerName, $headerValue); + } + } +} diff --git a/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/JsonVisitor.php b/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/JsonVisitor.php new file mode 100644 index 000000000..757e1c520 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/JsonVisitor.php @@ -0,0 +1,63 @@ +data = new \SplObjectStorage(); + } + + /** + * Set the Content-Type header to add to the request if JSON is added to the body. This visitor does not add a + * Content-Type header unless you specify one here. + * + * @param string $header Header to set when JSON is added (e.g. application/json) + * + * @return self + */ + public function setContentTypeHeader($header = 'application/json') + { + $this->jsonContentType = $header; + + return $this; + } + + public function visit(CommandInterface $command, RequestInterface $request, Parameter $param, $value) + { + if (isset($this->data[$command])) { + $json = $this->data[$command]; + } else { + $json = array(); + } + $json[$param->getWireName()] = $this->prepareValue($value, $param); + $this->data[$command] = $json; + } + + public function after(CommandInterface $command, RequestInterface $request) + { + if (isset($this->data[$command])) { + // Don't overwrite the Content-Type if one is set + if ($this->jsonContentType && !$request->hasHeader('Content-Type')) { + $request->setHeader('Content-Type', $this->jsonContentType); + } + + $request->setBody(json_encode($this->data[$command])); + unset($this->data[$command]); + } + } +} diff --git a/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/PostFieldVisitor.php b/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/PostFieldVisitor.php new file mode 100644 index 000000000..975850b74 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/PostFieldVisitor.php @@ -0,0 +1,18 @@ +setPostField($param->getWireName(), $this->prepareValue($value, $param)); + } +} diff --git a/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/PostFileVisitor.php b/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/PostFileVisitor.php new file mode 100644 index 000000000..0853ebe62 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/PostFileVisitor.php @@ -0,0 +1,24 @@ +filter($value); + if ($value instanceof PostFileInterface) { + $request->addPostFile($value); + } else { + $request->addPostFile($param->getWireName(), $value); + } + } +} diff --git a/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/QueryVisitor.php b/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/QueryVisitor.php new file mode 100644 index 000000000..315877aa0 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/QueryVisitor.php @@ -0,0 +1,18 @@ +getQuery()->set($param->getWireName(), $this->prepareValue($value, $param)); + } +} diff --git a/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/RequestVisitorInterface.php b/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/RequestVisitorInterface.php new file mode 100644 index 000000000..14e0b2d2b --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/RequestVisitorInterface.php @@ -0,0 +1,31 @@ +setResponseBody($value); + } +} diff --git a/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/XmlVisitor.php b/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/XmlVisitor.php new file mode 100644 index 000000000..5b7148787 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Request/XmlVisitor.php @@ -0,0 +1,252 @@ +data = new \SplObjectStorage(); + } + + /** + * Change the content-type header that is added when XML is found + * + * @param string $header Header to set when XML is found + * + * @return self + */ + public function setContentTypeHeader($header) + { + $this->contentType = $header; + + return $this; + } + + public function visit(CommandInterface $command, RequestInterface $request, Parameter $param, $value) + { + $xml = isset($this->data[$command]) + ? $this->data[$command] + : $this->createRootElement($param->getParent()); + $this->addXml($xml, $param, $value); + + $this->data[$command] = $xml; + } + + public function after(CommandInterface $command, RequestInterface $request) + { + $xml = null; + + // If data was found that needs to be serialized, then do so + if (isset($this->data[$command])) { + $xml = $this->finishDocument($this->data[$command]); + unset($this->data[$command]); + } else { + // Check if XML should always be sent for the command + $operation = $command->getOperation(); + if ($operation->getData('xmlAllowEmpty')) { + $xmlWriter = $this->createRootElement($operation); + $xml = $this->finishDocument($xmlWriter); + } + } + + if ($xml) { + // Don't overwrite the Content-Type if one is set + if ($this->contentType && !$request->hasHeader('Content-Type')) { + $request->setHeader('Content-Type', $this->contentType); + } + $request->setBody($xml); + } + } + + /** + * Create the root XML element to use with a request + * + * @param Operation $operation Operation object + * + * @return \XMLWriter + */ + protected function createRootElement(Operation $operation) + { + static $defaultRoot = array('name' => 'Request'); + // If no root element was specified, then just wrap the XML in 'Request' + $root = $operation->getData('xmlRoot') ?: $defaultRoot; + // Allow the XML declaration to be customized with xmlEncoding + $encoding = $operation->getData('xmlEncoding'); + + $xmlWriter = $this->startDocument($encoding); + + $xmlWriter->startElement($root['name']); + // Create the wrapping element with no namespaces if no namespaces were present + if (!empty($root['namespaces'])) { + // Create the wrapping element with an array of one or more namespaces + foreach ((array) $root['namespaces'] as $prefix => $uri) { + $nsLabel = 'xmlns'; + if (!is_numeric($prefix)) { + $nsLabel .= ':'.$prefix; + } + $xmlWriter->writeAttribute($nsLabel, $uri); + } + } + return $xmlWriter; + } + + /** + * Recursively build the XML body + * + * @param \XMLWriter $xmlWriter XML to modify + * @param Parameter $param API Parameter + * @param mixed $value Value to add + */ + protected function addXml(\XMLWriter $xmlWriter, Parameter $param, $value) + { + if ($value === null) { + return; + } + + $value = $param->filter($value); + $type = $param->getType(); + $name = $param->getWireName(); + $prefix = null; + $namespace = $param->getData('xmlNamespace'); + if (false !== strpos($name, ':')) { + list($prefix, $name) = explode(':', $name, 2); + } + + if ($type == 'object' || $type == 'array') { + if (!$param->getData('xmlFlattened')) { + $xmlWriter->startElementNS(null, $name, $namespace); + } + if ($param->getType() == 'array') { + $this->addXmlArray($xmlWriter, $param, $value); + } elseif ($param->getType() == 'object') { + $this->addXmlObject($xmlWriter, $param, $value); + } + if (!$param->getData('xmlFlattened')) { + $xmlWriter->endElement(); + } + return; + } + if ($param->getData('xmlAttribute')) { + $this->writeAttribute($xmlWriter, $prefix, $name, $namespace, $value); + } else { + $this->writeElement($xmlWriter, $prefix, $name, $namespace, $value); + } + } + + /** + * Write an attribute with namespace if used + * + * @param \XMLWriter $xmlWriter XMLWriter instance + * @param string $prefix Namespace prefix if any + * @param string $name Attribute name + * @param string $namespace The uri of the namespace + * @param string $value The attribute content + */ + protected function writeAttribute($xmlWriter, $prefix, $name, $namespace, $value) + { + if (empty($namespace)) { + $xmlWriter->writeAttribute($name, $value); + } else { + $xmlWriter->writeAttributeNS($prefix, $name, $namespace, $value); + } + } + + /** + * Write an element with namespace if used + * + * @param \XMLWriter $xmlWriter XML writer resource + * @param string $prefix Namespace prefix if any + * @param string $name Element name + * @param string $namespace The uri of the namespace + * @param string $value The element content + */ + protected function writeElement(\XMLWriter $xmlWriter, $prefix, $name, $namespace, $value) + { + $xmlWriter->startElementNS($prefix, $name, $namespace); + if (strpbrk($value, '<>&')) { + $xmlWriter->writeCData($value); + } else { + $xmlWriter->writeRaw($value); + } + $xmlWriter->endElement(); + } + + /** + * Create a new xml writer and start a document + * + * @param string $encoding document encoding + * + * @return \XMLWriter the writer resource + */ + protected function startDocument($encoding) + { + $xmlWriter = new \XMLWriter(); + $xmlWriter->openMemory(); + $xmlWriter->startDocument('1.0', $encoding); + + return $xmlWriter; + } + + /** + * End the document and return the output + * + * @param \XMLWriter $xmlWriter + * + * @return \string the writer resource + */ + protected function finishDocument($xmlWriter) + { + $xmlWriter->endDocument(); + + return $xmlWriter->outputMemory(); + } + + /** + * Add an array to the XML + */ + protected function addXmlArray(\XMLWriter $xmlWriter, Parameter $param, &$value) + { + if ($items = $param->getItems()) { + foreach ($value as $v) { + $this->addXml($xmlWriter, $items, $v); + } + } + } + + /** + * Add an object to the XML + */ + protected function addXmlObject(\XMLWriter $xmlWriter, Parameter $param, &$value) + { + $noAttributes = array(); + // add values which have attributes + foreach ($value as $name => $v) { + if ($property = $param->getProperty($name)) { + if ($property->getData('xmlAttribute')) { + $this->addXml($xmlWriter, $property, $v); + } else { + $noAttributes[] = array('value' => $v, 'property' => $property); + } + } + } + // now add values with no attributes + foreach ($noAttributes as $element) { + $this->addXml($xmlWriter, $element['property'], $element['value']); + } + } +} diff --git a/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Response/AbstractResponseVisitor.php b/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Response/AbstractResponseVisitor.php new file mode 100644 index 000000000..d87eeb945 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Response/AbstractResponseVisitor.php @@ -0,0 +1,26 @@ +getName()] = $param->filter($response->getBody()); + } +} diff --git a/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Response/HeaderVisitor.php b/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Response/HeaderVisitor.php new file mode 100644 index 000000000..0f8737cbd --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Response/HeaderVisitor.php @@ -0,0 +1,50 @@ +getType() == 'object' && $param->getAdditionalProperties() instanceof Parameter) { + $this->processPrefixedHeaders($response, $param, $value); + } else { + $value[$param->getName()] = $param->filter((string) $response->getHeader($param->getWireName())); + } + } + + /** + * Process a prefixed header array + * + * @param Response $response Response that contains the headers + * @param Parameter $param Parameter object + * @param array $value Value response array to modify + */ + protected function processPrefixedHeaders(Response $response, Parameter $param, &$value) + { + // Grab prefixed headers that should be placed into an array with the prefix stripped + if ($prefix = $param->getSentAs()) { + $container = $param->getName(); + $len = strlen($prefix); + // Find all matching headers and place them into the containing element + foreach ($response->getHeaders()->toArray() as $key => $header) { + if (stripos($key, $prefix) === 0) { + // Account for multi-value headers + $value[$container][substr($key, $len)] = count($header) == 1 ? end($header) : $header; + } + } + } + } +} diff --git a/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Response/JsonVisitor.php b/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Response/JsonVisitor.php new file mode 100644 index 000000000..a609ebd8c --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Response/JsonVisitor.php @@ -0,0 +1,93 @@ +getResponse()->json(); + } + + public function visit( + CommandInterface $command, + Response $response, + Parameter $param, + &$value, + $context = null + ) { + $name = $param->getName(); + $key = $param->getWireName(); + if (isset($value[$key])) { + $this->recursiveProcess($param, $value[$key]); + if ($key != $name) { + $value[$name] = $value[$key]; + unset($value[$key]); + } + } + } + + /** + * Recursively process a parameter while applying filters + * + * @param Parameter $param API parameter being validated + * @param mixed $value Value to validate and process. The value may change during this process. + */ + protected function recursiveProcess(Parameter $param, &$value) + { + if ($value === null) { + return; + } + + if (is_array($value)) { + $type = $param->getType(); + if ($type == 'array') { + foreach ($value as &$item) { + $this->recursiveProcess($param->getItems(), $item); + } + } elseif ($type == 'object' && !isset($value[0])) { + // On the above line, we ensure that the array is associative and not numerically indexed + $knownProperties = array(); + if ($properties = $param->getProperties()) { + foreach ($properties as $property) { + $name = $property->getName(); + $key = $property->getWireName(); + $knownProperties[$name] = 1; + if (isset($value[$key])) { + $this->recursiveProcess($property, $value[$key]); + if ($key != $name) { + $value[$name] = $value[$key]; + unset($value[$key]); + } + } + } + } + + // Remove any unknown and potentially unsafe properties + if ($param->getAdditionalProperties() === false) { + $value = array_intersect_key($value, $knownProperties); + } elseif (($additional = $param->getAdditionalProperties()) !== true) { + // Validate and filter additional properties + foreach ($value as &$v) { + $this->recursiveProcess($additional, $v); + } + } + } + } + + $value = $param->filter($value); + } +} diff --git a/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Response/ReasonPhraseVisitor.php b/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Response/ReasonPhraseVisitor.php new file mode 100644 index 000000000..1b10ebce7 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Response/ReasonPhraseVisitor.php @@ -0,0 +1,23 @@ +getName()] = $response->getReasonPhrase(); + } +} diff --git a/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Response/ResponseVisitorInterface.php b/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Response/ResponseVisitorInterface.php new file mode 100644 index 000000000..033f40c3f --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Response/ResponseVisitorInterface.php @@ -0,0 +1,46 @@ +getName()] = $response->getStatusCode(); + } +} diff --git a/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Response/XmlVisitor.php b/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Response/XmlVisitor.php new file mode 100644 index 000000000..64affbc34 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/Response/XmlVisitor.php @@ -0,0 +1,149 @@ +getResponse()->xml()), true); + } + + public function visit( + CommandInterface $command, + Response $response, + Parameter $param, + &$value, + $context = null + ) { + $sentAs = $param->getWireName(); + $name = $param->getName(); + if (isset($value[$sentAs])) { + $this->recursiveProcess($param, $value[$sentAs]); + if ($name != $sentAs) { + $value[$name] = $value[$sentAs]; + unset($value[$sentAs]); + } + } + } + + /** + * Recursively process a parameter while applying filters + * + * @param Parameter $param API parameter being processed + * @param mixed $value Value to validate and process. The value may change during this process. + */ + protected function recursiveProcess(Parameter $param, &$value) + { + $type = $param->getType(); + + if (!is_array($value)) { + if ($type == 'array') { + // Cast to an array if the value was a string, but should be an array + $this->recursiveProcess($param->getItems(), $value); + $value = array($value); + } + } elseif ($type == 'object') { + $this->processObject($param, $value); + } elseif ($type == 'array') { + $this->processArray($param, $value); + } + + if ($value !== null) { + $value = $param->filter($value); + } + } + + /** + * Process an array + * + * @param Parameter $param API parameter being parsed + * @param mixed $value Value to process + */ + protected function processArray(Parameter $param, &$value) + { + // Convert the node if it was meant to be an array + if (!isset($value[0])) { + // Collections fo nodes are sometimes wrapped in an additional array. For example: + // 12 should become: + // array('Items' => array(array('a' => 1), array('a' => 2)) + // Some nodes are not wrapped. For example: 12 + // should become array('Foo' => array(array('a' => 1), array('a' => 2)) + if ($param->getItems() && isset($value[$param->getItems()->getWireName()])) { + // Account for the case of a collection wrapping wrapped nodes: Items => Item[] + $value = $value[$param->getItems()->getWireName()]; + // If the wrapped node only had one value, then make it an array of nodes + if (!isset($value[0]) || !is_array($value)) { + $value = array($value); + } + } elseif (!empty($value)) { + // Account for repeated nodes that must be an array: Foo => Baz, Foo => Baz, but only if the + // value is set and not empty + $value = array($value); + } + } + + foreach ($value as &$item) { + $this->recursiveProcess($param->getItems(), $item); + } + } + + /** + * Process an object + * + * @param Parameter $param API parameter being parsed + * @param mixed $value Value to process + */ + protected function processObject(Parameter $param, &$value) + { + // Ensure that the array is associative and not numerically indexed + if (!isset($value[0]) && ($properties = $param->getProperties())) { + $knownProperties = array(); + foreach ($properties as $property) { + $name = $property->getName(); + $sentAs = $property->getWireName(); + $knownProperties[$name] = 1; + if ($property->getData('xmlAttribute')) { + $this->processXmlAttribute($property, $value); + } elseif (isset($value[$sentAs])) { + $this->recursiveProcess($property, $value[$sentAs]); + if ($name != $sentAs) { + $value[$name] = $value[$sentAs]; + unset($value[$sentAs]); + } + } + } + + // Remove any unknown and potentially unsafe properties + if ($param->getAdditionalProperties() === false) { + $value = array_intersect_key($value, $knownProperties); + } + } + } + + /** + * Process an XML attribute property + * + * @param Parameter $property Property to process + * @param array $value Value to process and update + */ + protected function processXmlAttribute(Parameter $property, array &$value) + { + $sentAs = $property->getWireName(); + if (isset($value['@attributes'][$sentAs])) { + $value[$property->getName()] = $value['@attributes'][$sentAs]; + unset($value['@attributes'][$sentAs]); + if (empty($value['@attributes'])) { + unset($value['@attributes']); + } + } + } +} diff --git a/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/VisitorFlyweight.php b/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/VisitorFlyweight.php new file mode 100644 index 000000000..74cb62813 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Service/Command/LocationVisitor/VisitorFlyweight.php @@ -0,0 +1,138 @@ + 'Guzzle\Service\Command\LocationVisitor\Request\BodyVisitor', + 'request.header' => 'Guzzle\Service\Command\LocationVisitor\Request\HeaderVisitor', + 'request.json' => 'Guzzle\Service\Command\LocationVisitor\Request\JsonVisitor', + 'request.postField' => 'Guzzle\Service\Command\LocationVisitor\Request\PostFieldVisitor', + 'request.postFile' => 'Guzzle\Service\Command\LocationVisitor\Request\PostFileVisitor', + 'request.query' => 'Guzzle\Service\Command\LocationVisitor\Request\QueryVisitor', + 'request.response_body' => 'Guzzle\Service\Command\LocationVisitor\Request\ResponseBodyVisitor', + 'request.responseBody' => 'Guzzle\Service\Command\LocationVisitor\Request\ResponseBodyVisitor', + 'request.xml' => 'Guzzle\Service\Command\LocationVisitor\Request\XmlVisitor', + 'response.body' => 'Guzzle\Service\Command\LocationVisitor\Response\BodyVisitor', + 'response.header' => 'Guzzle\Service\Command\LocationVisitor\Response\HeaderVisitor', + 'response.json' => 'Guzzle\Service\Command\LocationVisitor\Response\JsonVisitor', + 'response.reasonPhrase' => 'Guzzle\Service\Command\LocationVisitor\Response\ReasonPhraseVisitor', + 'response.statusCode' => 'Guzzle\Service\Command\LocationVisitor\Response\StatusCodeVisitor', + 'response.xml' => 'Guzzle\Service\Command\LocationVisitor\Response\XmlVisitor' + ); + + /** @var array Array of mappings of location names to classes */ + protected $mappings; + + /** @var array Cache of instantiated visitors */ + protected $cache = array(); + + /** + * @return self + * @codeCoverageIgnore + */ + public static function getInstance() + { + if (!self::$instance) { + self::$instance = new self(); + } + + return self::$instance; + } + + /** + * @param array $mappings Array mapping request.name and response.name to location visitor classes. Leave null to + * use the default values. + */ + public function __construct(array $mappings = null) + { + $this->mappings = $mappings === null ? self::$defaultMappings : $mappings; + } + + /** + * Get an instance of a request visitor by location name + * + * @param string $visitor Visitor name + * + * @return RequestVisitorInterface + */ + public function getRequestVisitor($visitor) + { + return $this->getKey('request.' . $visitor); + } + + /** + * Get an instance of a response visitor by location name + * + * @param string $visitor Visitor name + * + * @return ResponseVisitorInterface + */ + public function getResponseVisitor($visitor) + { + return $this->getKey('response.' . $visitor); + } + + /** + * Add a response visitor to the factory by name + * + * @param string $name Name of the visitor + * @param RequestVisitorInterface $visitor Visitor to add + * + * @return self + */ + public function addRequestVisitor($name, RequestVisitorInterface $visitor) + { + $this->cache['request.' . $name] = $visitor; + + return $this; + } + + /** + * Add a response visitor to the factory by name + * + * @param string $name Name of the visitor + * @param ResponseVisitorInterface $visitor Visitor to add + * + * @return self + */ + public function addResponseVisitor($name, ResponseVisitorInterface $visitor) + { + $this->cache['response.' . $name] = $visitor; + + return $this; + } + + /** + * Get a visitor by key value name + * + * @param string $key Key name to retrieve + * + * @return mixed + * @throws InvalidArgumentException + */ + private function getKey($key) + { + if (!isset($this->cache[$key])) { + if (!isset($this->mappings[$key])) { + list($type, $name) = explode('.', $key); + throw new InvalidArgumentException("No {$type} visitor has been mapped for {$name}"); + } + $this->cache[$key] = new $this->mappings[$key]; + } + + return $this->cache[$key]; + } +} diff --git a/guzzle/guzzle/src/Guzzle/Service/Command/OperationCommand.php b/guzzle/guzzle/src/Guzzle/Service/Command/OperationCommand.php new file mode 100644 index 000000000..0748b5af0 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Service/Command/OperationCommand.php @@ -0,0 +1,89 @@ +responseParser = $parser; + + return $this; + } + + /** + * Set the request serializer used with the command + * + * @param RequestSerializerInterface $serializer Request serializer + * + * @return self + */ + public function setRequestSerializer(RequestSerializerInterface $serializer) + { + $this->requestSerializer = $serializer; + + return $this; + } + + /** + * Get the request serializer used with the command + * + * @return RequestSerializerInterface + */ + public function getRequestSerializer() + { + if (!$this->requestSerializer) { + // Use the default request serializer if none was found + $this->requestSerializer = DefaultRequestSerializer::getInstance(); + } + + return $this->requestSerializer; + } + + /** + * Get the response parser used for the operation + * + * @return ResponseParserInterface + */ + public function getResponseParser() + { + if (!$this->responseParser) { + // Use the default response parser if none was found + $this->responseParser = OperationResponseParser::getInstance(); + } + + return $this->responseParser; + } + + protected function build() + { + // Prepare and serialize the request + $this->request = $this->getRequestSerializer()->prepare($this); + } + + protected function process() + { + // Do not process the response if 'command.response_processing' is set to 'raw' + $this->result = $this[self::RESPONSE_PROCESSING] == self::TYPE_RAW + ? $this->request->getResponse() + : $this->getResponseParser()->parse($this); + } +} diff --git a/guzzle/guzzle/src/Guzzle/Service/Command/OperationResponseParser.php b/guzzle/guzzle/src/Guzzle/Service/Command/OperationResponseParser.php new file mode 100644 index 000000000..ca00bc062 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Service/Command/OperationResponseParser.php @@ -0,0 +1,195 @@ +factory = $factory; + $this->schemaInModels = $schemaInModels; + } + + /** + * Add a location visitor to the command + * + * @param string $location Location to associate with the visitor + * @param ResponseVisitorInterface $visitor Visitor to attach + * + * @return self + */ + public function addVisitor($location, ResponseVisitorInterface $visitor) + { + $this->factory->addResponseVisitor($location, $visitor); + + return $this; + } + + protected function handleParsing(CommandInterface $command, Response $response, $contentType) + { + $operation = $command->getOperation(); + $type = $operation->getResponseType(); + $model = null; + + if ($type == OperationInterface::TYPE_MODEL) { + $model = $operation->getServiceDescription()->getModel($operation->getResponseClass()); + } elseif ($type == OperationInterface::TYPE_CLASS) { + return $this->parseClass($command); + } + + if (!$model) { + // Return basic processing if the responseType is not model or the model cannot be found + return parent::handleParsing($command, $response, $contentType); + } elseif ($command[AbstractCommand::RESPONSE_PROCESSING] != AbstractCommand::TYPE_MODEL) { + // Returns a model with no visiting if the command response processing is not model + return new Model(parent::handleParsing($command, $response, $contentType)); + } else { + // Only inject the schema into the model if "schemaInModel" is true + return new Model($this->visitResult($model, $command, $response), $this->schemaInModels ? $model : null); + } + } + + /** + * Parse a class object + * + * @param CommandInterface $command Command to parse into an object + * + * @return mixed + * @throws ResponseClassException + */ + protected function parseClass(CommandInterface $command) + { + // Emit the operation.parse_class event. If a listener injects a 'result' property, then that will be the result + $event = new CreateResponseClassEvent(array('command' => $command)); + $command->getClient()->getEventDispatcher()->dispatch('command.parse_response', $event); + if ($result = $event->getResult()) { + return $result; + } + + $className = $command->getOperation()->getResponseClass(); + if (!method_exists($className, 'fromCommand')) { + throw new ResponseClassException("{$className} must exist and implement a static fromCommand() method"); + } + + return $className::fromCommand($command); + } + + /** + * Perform transformations on the result array + * + * @param Parameter $model Model that defines the structure + * @param CommandInterface $command Command that performed the operation + * @param Response $response Response received + * + * @return array Returns the array of result data + */ + protected function visitResult(Parameter $model, CommandInterface $command, Response $response) + { + $foundVisitors = $result = $knownProps = array(); + $props = $model->getProperties(); + + foreach ($props as $schema) { + if ($location = $schema->getLocation()) { + // Trigger the before method on the first found visitor of this type + if (!isset($foundVisitors[$location])) { + $foundVisitors[$location] = $this->factory->getResponseVisitor($location); + $foundVisitors[$location]->before($command, $result); + } + } + } + + // Visit additional properties when it is an actual schema + if (($additional = $model->getAdditionalProperties()) instanceof Parameter) { + $this->visitAdditionalProperties($model, $command, $response, $additional, $result, $foundVisitors); + } + + // Apply the parameter value with the location visitor + foreach ($props as $schema) { + $knownProps[$schema->getName()] = 1; + if ($location = $schema->getLocation()) { + $foundVisitors[$location]->visit($command, $response, $schema, $result); + } + } + + // Remove any unknown and potentially unsafe top-level properties + if ($additional === false) { + $result = array_intersect_key($result, $knownProps); + } + + // Call the after() method of each found visitor + foreach ($foundVisitors as $visitor) { + $visitor->after($command); + } + + return $result; + } + + protected function visitAdditionalProperties( + Parameter $model, + CommandInterface $command, + Response $response, + Parameter $additional, + &$result, + array &$foundVisitors + ) { + // Only visit when a location is specified + if ($location = $additional->getLocation()) { + if (!isset($foundVisitors[$location])) { + $foundVisitors[$location] = $this->factory->getResponseVisitor($location); + $foundVisitors[$location]->before($command, $result); + } + // Only traverse if an array was parsed from the before() visitors + if (is_array($result)) { + // Find each additional property + foreach (array_keys($result) as $key) { + // Check if the model actually knows this property. If so, then it is not additional + if (!$model->getProperty($key)) { + // Set the name to the key so that we can parse it with each visitor + $additional->setName($key); + $foundVisitors[$location]->visit($command, $response, $additional, $result); + } + } + // Reset the additionalProperties name to null + $additional->setName(null); + } + } + } +} diff --git a/guzzle/guzzle/src/Guzzle/Service/Command/RequestSerializerInterface.php b/guzzle/guzzle/src/Guzzle/Service/Command/RequestSerializerInterface.php new file mode 100644 index 000000000..60b9334d4 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Service/Command/RequestSerializerInterface.php @@ -0,0 +1,21 @@ + true, 'httpMethod' => true, 'uri' => true, 'class' => true, 'responseClass' => true, + 'responseType' => true, 'responseNotes' => true, 'notes' => true, 'summary' => true, 'documentationUrl' => true, + 'deprecated' => true, 'data' => true, 'parameters' => true, 'additionalParameters' => true, + 'errorResponses' => true + ); + + /** @var array Parameters */ + protected $parameters = array(); + + /** @var Parameter Additional parameters schema */ + protected $additionalParameters; + + /** @var string Name of the command */ + protected $name; + + /** @var string HTTP method */ + protected $httpMethod; + + /** @var string This is a short summary of what the operation does */ + protected $summary; + + /** @var string A longer text field to explain the behavior of the operation. */ + protected $notes; + + /** @var string Reference URL providing more information about the operation */ + protected $documentationUrl; + + /** @var string HTTP URI of the command */ + protected $uri; + + /** @var string Class of the command object */ + protected $class; + + /** @var string This is what is returned from the method */ + protected $responseClass; + + /** @var string Type information about the response */ + protected $responseType; + + /** @var string Information about the response returned by the operation */ + protected $responseNotes; + + /** @var bool Whether or not the command is deprecated */ + protected $deprecated; + + /** @var array Array of errors that could occur when running the command */ + protected $errorResponses; + + /** @var ServiceDescriptionInterface */ + protected $description; + + /** @var array Extra operation information */ + protected $data; + + /** + * Builds an Operation object using an array of configuration data: + * - name: (string) Name of the command + * - httpMethod: (string) HTTP method of the operation + * - uri: (string) URI template that can create a relative or absolute URL + * - class: (string) Concrete class that implements this command + * - parameters: (array) Associative array of parameters for the command. {@see Parameter} for information. + * - summary: (string) This is a short summary of what the operation does + * - notes: (string) A longer text field to explain the behavior of the operation. + * - documentationUrl: (string) Reference URL providing more information about the operation + * - responseClass: (string) This is what is returned from the method. Can be a primitive, PSR-0 compliant + * class name, or model. + * - responseNotes: (string) Information about the response returned by the operation + * - responseType: (string) One of 'primitive', 'class', 'model', or 'documentation'. If not specified, this + * value will be automatically inferred based on whether or not there is a model matching the + * name, if a matching PSR-0 compliant class name is found, or set to 'primitive' by default. + * - deprecated: (bool) Set to true if this is a deprecated command + * - errorResponses: (array) Errors that could occur when executing the command. Array of hashes, each with a + * 'code' (the HTTP response code), 'phrase' (response reason phrase or description of the + * error), and 'class' (a custom exception class that would be thrown if the error is + * encountered). + * - data: (array) Any extra data that might be used to help build or serialize the operation + * - additionalParameters: (null|array) Parameter schema to use when an option is passed to the operation that is + * not in the schema + * + * @param array $config Array of configuration data + * @param ServiceDescriptionInterface $description Service description used to resolve models if $ref tags are found + */ + public function __construct(array $config = array(), ServiceDescriptionInterface $description = null) + { + $this->description = $description; + + // Get the intersection of the available properties and properties set on the operation + foreach (array_intersect_key($config, self::$properties) as $key => $value) { + $this->{$key} = $value; + } + + $this->class = $this->class ?: self::DEFAULT_COMMAND_CLASS; + $this->deprecated = (bool) $this->deprecated; + $this->errorResponses = $this->errorResponses ?: array(); + $this->data = $this->data ?: array(); + + if (!$this->responseClass) { + $this->responseClass = 'array'; + $this->responseType = 'primitive'; + } elseif ($this->responseType) { + // Set the response type to perform validation + $this->setResponseType($this->responseType); + } else { + // A response class was set and no response type was set, so guess what the type is + $this->inferResponseType(); + } + + // Parameters need special handling when adding + if ($this->parameters) { + foreach ($this->parameters as $name => $param) { + if ($param instanceof Parameter) { + $param->setName($name)->setParent($this); + } elseif (is_array($param)) { + $param['name'] = $name; + $this->addParam(new Parameter($param, $this->description)); + } + } + } + + if ($this->additionalParameters) { + if ($this->additionalParameters instanceof Parameter) { + $this->additionalParameters->setParent($this); + } elseif (is_array($this->additionalParameters)) { + $this->setadditionalParameters(new Parameter($this->additionalParameters, $this->description)); + } + } + } + + public function toArray() + { + $result = array(); + // Grab valid properties and filter out values that weren't set + foreach (array_keys(self::$properties) as $check) { + if ($value = $this->{$check}) { + $result[$check] = $value; + } + } + // Remove the name property + unset($result['name']); + // Parameters need to be converted to arrays + $result['parameters'] = array(); + foreach ($this->parameters as $key => $param) { + $result['parameters'][$key] = $param->toArray(); + } + // Additional parameters need to be cast to an array + if ($this->additionalParameters instanceof Parameter) { + $result['additionalParameters'] = $this->additionalParameters->toArray(); + } + + return $result; + } + + public function getServiceDescription() + { + return $this->description; + } + + public function setServiceDescription(ServiceDescriptionInterface $description) + { + $this->description = $description; + + return $this; + } + + public function getParams() + { + return $this->parameters; + } + + public function getParamNames() + { + return array_keys($this->parameters); + } + + public function hasParam($name) + { + return isset($this->parameters[$name]); + } + + public function getParam($param) + { + return isset($this->parameters[$param]) ? $this->parameters[$param] : null; + } + + /** + * Add a parameter to the command + * + * @param Parameter $param Parameter to add + * + * @return self + */ + public function addParam(Parameter $param) + { + $this->parameters[$param->getName()] = $param; + $param->setParent($this); + + return $this; + } + + /** + * Remove a parameter from the command + * + * @param string $name Name of the parameter to remove + * + * @return self + */ + public function removeParam($name) + { + unset($this->parameters[$name]); + + return $this; + } + + public function getHttpMethod() + { + return $this->httpMethod; + } + + /** + * Set the HTTP method of the command + * + * @param string $httpMethod Method to set + * + * @return self + */ + public function setHttpMethod($httpMethod) + { + $this->httpMethod = $httpMethod; + + return $this; + } + + public function getClass() + { + return $this->class; + } + + /** + * Set the concrete class of the command + * + * @param string $className Concrete class name + * + * @return self + */ + public function setClass($className) + { + $this->class = $className; + + return $this; + } + + public function getName() + { + return $this->name; + } + + /** + * Set the name of the command + * + * @param string $name Name of the command + * + * @return self + */ + public function setName($name) + { + $this->name = $name; + + return $this; + } + + public function getSummary() + { + return $this->summary; + } + + /** + * Set a short summary of what the operation does + * + * @param string $summary Short summary of the operation + * + * @return self + */ + public function setSummary($summary) + { + $this->summary = $summary; + + return $this; + } + + public function getNotes() + { + return $this->notes; + } + + /** + * Set a longer text field to explain the behavior of the operation. + * + * @param string $notes Notes on the operation + * + * @return self + */ + public function setNotes($notes) + { + $this->notes = $notes; + + return $this; + } + + public function getDocumentationUrl() + { + return $this->documentationUrl; + } + + /** + * Set the URL pointing to additional documentation on the command + * + * @param string $docUrl Documentation URL + * + * @return self + */ + public function setDocumentationUrl($docUrl) + { + $this->documentationUrl = $docUrl; + + return $this; + } + + public function getResponseClass() + { + return $this->responseClass; + } + + /** + * Set what is returned from the method. Can be a primitive, class name, or model. For example: 'array', + * 'Guzzle\\Foo\\Baz', or 'MyModelName' (to reference a model by ID). + * + * @param string $responseClass Type of response + * + * @return self + */ + public function setResponseClass($responseClass) + { + $this->responseClass = $responseClass; + $this->inferResponseType(); + + return $this; + } + + public function getResponseType() + { + return $this->responseType; + } + + /** + * Set qualifying information about the responseClass. One of 'primitive', 'class', 'model', or 'documentation' + * + * @param string $responseType Response type information + * + * @return self + * @throws InvalidArgumentException + */ + public function setResponseType($responseType) + { + static $types = array( + self::TYPE_PRIMITIVE => true, + self::TYPE_CLASS => true, + self::TYPE_MODEL => true, + self::TYPE_DOCUMENTATION => true + ); + if (!isset($types[$responseType])) { + throw new InvalidArgumentException('responseType must be one of ' . implode(', ', array_keys($types))); + } + + $this->responseType = $responseType; + + return $this; + } + + public function getResponseNotes() + { + return $this->responseNotes; + } + + /** + * Set notes about the response of the operation + * + * @param string $notes Response notes + * + * @return self + */ + public function setResponseNotes($notes) + { + $this->responseNotes = $notes; + + return $this; + } + + public function getDeprecated() + { + return $this->deprecated; + } + + /** + * Set whether or not the command is deprecated + * + * @param bool $isDeprecated Set to true to mark as deprecated + * + * @return self + */ + public function setDeprecated($isDeprecated) + { + $this->deprecated = $isDeprecated; + + return $this; + } + + public function getUri() + { + return $this->uri; + } + + /** + * Set the URI template of the command + * + * @param string $uri URI template to set + * + * @return self + */ + public function setUri($uri) + { + $this->uri = $uri; + + return $this; + } + + public function getErrorResponses() + { + return $this->errorResponses; + } + + /** + * Add an error to the command + * + * @param string $code HTTP response code + * @param string $reason HTTP response reason phrase or information about the error + * @param string $class Exception class associated with the error + * + * @return self + */ + public function addErrorResponse($code, $reason, $class) + { + $this->errorResponses[] = array('code' => $code, 'reason' => $reason, 'class' => $class); + + return $this; + } + + /** + * Set all of the error responses of the operation + * + * @param array $errorResponses Hash of error name to a hash containing a code, reason, class + * + * @return self + */ + public function setErrorResponses(array $errorResponses) + { + $this->errorResponses = $errorResponses; + + return $this; + } + + public function getData($name) + { + return isset($this->data[$name]) ? $this->data[$name] : null; + } + + /** + * Set a particular data point on the operation + * + * @param string $name Name of the data value + * @param mixed $value Value to set + * + * @return self + */ + public function setData($name, $value) + { + $this->data[$name] = $value; + + return $this; + } + + /** + * Get the additionalParameters of the operation + * + * @return Parameter|null + */ + public function getAdditionalParameters() + { + return $this->additionalParameters; + } + + /** + * Set the additionalParameters of the operation + * + * @param Parameter|null $parameter Parameter to set + * + * @return self + */ + public function setAdditionalParameters($parameter) + { + if ($this->additionalParameters = $parameter) { + $this->additionalParameters->setParent($this); + } + + return $this; + } + + /** + * Infer the response type from the responseClass value + */ + protected function inferResponseType() + { + static $primitives = array('array' => 1, 'boolean' => 1, 'string' => 1, 'integer' => 1, '' => 1); + if (isset($primitives[$this->responseClass])) { + $this->responseType = self::TYPE_PRIMITIVE; + } elseif ($this->description && $this->description->hasModel($this->responseClass)) { + $this->responseType = self::TYPE_MODEL; + } else { + $this->responseType = self::TYPE_CLASS; + } + } +} diff --git a/guzzle/guzzle/src/Guzzle/Service/Description/OperationInterface.php b/guzzle/guzzle/src/Guzzle/Service/Description/OperationInterface.php new file mode 100644 index 000000000..4de41bd67 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Service/Description/OperationInterface.php @@ -0,0 +1,159 @@ +getModel($data['$ref'])) { + $data = $model->toArray() + $data; + } + } elseif (isset($data['extends'])) { + // If this parameter extends from another parameter then start with the actual data + // union in the parent's data (e.g. actual supersedes parent) + if ($extends = $description->getModel($data['extends'])) { + $data += $extends->toArray(); + } + } + } + + // Pull configuration data into the parameter + foreach ($data as $key => $value) { + $this->{$key} = $value; + } + + $this->serviceDescription = $description; + $this->required = (bool) $this->required; + $this->data = (array) $this->data; + + if ($this->filters) { + $this->setFilters((array) $this->filters); + } + + if ($this->type == 'object' && $this->additionalProperties === null) { + $this->additionalProperties = true; + } + } + + /** + * Convert the object to an array + * + * @return array + */ + public function toArray() + { + static $checks = array('required', 'description', 'static', 'type', 'format', 'instanceOf', 'location', 'sentAs', + 'pattern', 'minimum', 'maximum', 'minItems', 'maxItems', 'minLength', 'maxLength', 'data', 'enum', + 'filters'); + + $result = array(); + + // Anything that is in the `Items` attribute of an array *must* include it's name if available + if ($this->parent instanceof self && $this->parent->getType() == 'array' && isset($this->name)) { + $result['name'] = $this->name; + } + + foreach ($checks as $c) { + if ($value = $this->{$c}) { + $result[$c] = $value; + } + } + + if ($this->default !== null) { + $result['default'] = $this->default; + } + + if ($this->items !== null) { + $result['items'] = $this->getItems()->toArray(); + } + + if ($this->additionalProperties !== null) { + $result['additionalProperties'] = $this->getAdditionalProperties(); + if ($result['additionalProperties'] instanceof self) { + $result['additionalProperties'] = $result['additionalProperties']->toArray(); + } + } + + if ($this->type == 'object' && $this->properties) { + $result['properties'] = array(); + foreach ($this->getProperties() as $name => $property) { + $result['properties'][$name] = $property->toArray(); + } + } + + return $result; + } + + /** + * Get the default or static value of the command based on a value + * + * @param string $value Value that is currently set + * + * @return mixed Returns the value, a static value if one is present, or a default value + */ + public function getValue($value) + { + if ($this->static || ($this->default !== null && $value === null)) { + return $this->default; + } + + return $value; + } + + /** + * Run a value through the filters OR format attribute associated with the parameter + * + * @param mixed $value Value to filter + * + * @return mixed Returns the filtered value + */ + public function filter($value) + { + // Formats are applied exclusively and supersed filters + if ($this->format) { + return SchemaFormatter::format($this->format, $value); + } + + // Convert Boolean values + if ($this->type == 'boolean' && !is_bool($value)) { + $value = filter_var($value, FILTER_VALIDATE_BOOLEAN); + } + + // Apply filters to the value + if ($this->filters) { + foreach ($this->filters as $filter) { + if (is_array($filter)) { + // Convert complex filters that hold value place holders + foreach ($filter['args'] as &$data) { + if ($data == '@value') { + $data = $value; + } elseif ($data == '@api') { + $data = $this; + } + } + $value = call_user_func_array($filter['method'], $filter['args']); + } else { + $value = call_user_func($filter, $value); + } + } + } + + return $value; + } + + /** + * Get the name of the parameter + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Get the key of the parameter, where sentAs will supersede name if it is set + * + * @return string + */ + public function getWireName() + { + return $this->sentAs ?: $this->name; + } + + /** + * Set the name of the parameter + * + * @param string $name Name to set + * + * @return self + */ + public function setName($name) + { + $this->name = $name; + + return $this; + } + + /** + * Get the type(s) of the parameter + * + * @return string|array + */ + public function getType() + { + return $this->type; + } + + /** + * Set the type(s) of the parameter + * + * @param string|array $type Type of parameter or array of simple types used in a union + * + * @return self + */ + public function setType($type) + { + $this->type = $type; + + return $this; + } + + /** + * Get if the parameter is required + * + * @return bool + */ + public function getRequired() + { + return $this->required; + } + + /** + * Set if the parameter is required + * + * @param bool $isRequired Whether or not the parameter is required + * + * @return self + */ + public function setRequired($isRequired) + { + $this->required = (bool) $isRequired; + + return $this; + } + + /** + * Get the default value of the parameter + * + * @return string|null + */ + public function getDefault() + { + return $this->default; + } + + /** + * Set the default value of the parameter + * + * @param string|null $default Default value to set + * + * @return self + */ + public function setDefault($default) + { + $this->default = $default; + + return $this; + } + + /** + * Get the description of the parameter + * + * @return string|null + */ + public function getDescription() + { + return $this->description; + } + + /** + * Set the description of the parameter + * + * @param string $description Description + * + * @return self + */ + public function setDescription($description) + { + $this->description = $description; + + return $this; + } + + /** + * Get the minimum acceptable value for an integer + * + * @return int|null + */ + public function getMinimum() + { + return $this->minimum; + } + + /** + * Set the minimum acceptable value for an integer + * + * @param int|null $min Minimum + * + * @return self + */ + public function setMinimum($min) + { + $this->minimum = $min; + + return $this; + } + + /** + * Get the maximum acceptable value for an integer + * + * @return int|null + */ + public function getMaximum() + { + return $this->maximum; + } + + /** + * Set the maximum acceptable value for an integer + * + * @param int $max Maximum + * + * @return self + */ + public function setMaximum($max) + { + $this->maximum = $max; + + return $this; + } + + /** + * Get the minimum allowed length of a string value + * + * @return int + */ + public function getMinLength() + { + return $this->minLength; + } + + /** + * Set the minimum allowed length of a string value + * + * @param int|null $min Minimum + * + * @return self + */ + public function setMinLength($min) + { + $this->minLength = $min; + + return $this; + } + + /** + * Get the maximum allowed length of a string value + * + * @return int|null + */ + public function getMaxLength() + { + return $this->maxLength; + } + + /** + * Set the maximum allowed length of a string value + * + * @param int $max Maximum length + * + * @return self + */ + public function setMaxLength($max) + { + $this->maxLength = $max; + + return $this; + } + + /** + * Get the maximum allowed number of items in an array value + * + * @return int|null + */ + public function getMaxItems() + { + return $this->maxItems; + } + + /** + * Set the maximum allowed number of items in an array value + * + * @param int $max Maximum + * + * @return self + */ + public function setMaxItems($max) + { + $this->maxItems = $max; + + return $this; + } + + /** + * Get the minimum allowed number of items in an array value + * + * @return int + */ + public function getMinItems() + { + return $this->minItems; + } + + /** + * Set the minimum allowed number of items in an array value + * + * @param int|null $min Minimum + * + * @return self + */ + public function setMinItems($min) + { + $this->minItems = $min; + + return $this; + } + + /** + * Get the location of the parameter + * + * @return string|null + */ + public function getLocation() + { + return $this->location; + } + + /** + * Set the location of the parameter + * + * @param string|null $location Location of the parameter + * + * @return self + */ + public function setLocation($location) + { + $this->location = $location; + + return $this; + } + + /** + * Get the sentAs attribute of the parameter that used with locations to sentAs an attribute when it is being + * applied to a location. + * + * @return string|null + */ + public function getSentAs() + { + return $this->sentAs; + } + + /** + * Set the sentAs attribute + * + * @param string|null $name Name of the value as it is sent over the wire + * + * @return self + */ + public function setSentAs($name) + { + $this->sentAs = $name; + + return $this; + } + + /** + * Retrieve a known property from the parameter by name or a data property by name. When not specific name value + * is specified, all data properties will be returned. + * + * @param string|null $name Specify a particular property name to retrieve + * + * @return array|mixed|null + */ + public function getData($name = null) + { + if (!$name) { + return $this->data; + } + + if (isset($this->data[$name])) { + return $this->data[$name]; + } elseif (isset($this->{$name})) { + return $this->{$name}; + } + + return null; + } + + /** + * Set the extra data properties of the parameter or set a specific extra property + * + * @param string|array|null $nameOrData The name of a specific extra to set or an array of extras to set + * @param mixed|null $data When setting a specific extra property, specify the data to set for it + * + * @return self + */ + public function setData($nameOrData, $data = null) + { + if (is_array($nameOrData)) { + $this->data = $nameOrData; + } else { + $this->data[$nameOrData] = $data; + } + + return $this; + } + + /** + * Get whether or not the default value can be changed + * + * @return mixed|null + */ + public function getStatic() + { + return $this->static; + } + + /** + * Set to true if the default value cannot be changed + * + * @param bool $static True or false + * + * @return self + */ + public function setStatic($static) + { + $this->static = (bool) $static; + + return $this; + } + + /** + * Get an array of filters used by the parameter + * + * @return array + */ + public function getFilters() + { + return $this->filters ?: array(); + } + + /** + * Set the array of filters used by the parameter + * + * @param array $filters Array of functions to use as filters + * + * @return self + */ + public function setFilters(array $filters) + { + $this->filters = array(); + foreach ($filters as $filter) { + $this->addFilter($filter); + } + + return $this; + } + + /** + * Add a filter to the parameter + * + * @param string|array $filter Method to filter the value through + * + * @return self + * @throws InvalidArgumentException + */ + public function addFilter($filter) + { + if (is_array($filter)) { + if (!isset($filter['method'])) { + throw new InvalidArgumentException('A [method] value must be specified for each complex filter'); + } + } + + if (!$this->filters) { + $this->filters = array($filter); + } else { + $this->filters[] = $filter; + } + + return $this; + } + + /** + * Get the parent object (an {@see OperationInterface} or {@see Parameter} + * + * @return OperationInterface|Parameter|null + */ + public function getParent() + { + return $this->parent; + } + + /** + * Set the parent object of the parameter + * + * @param OperationInterface|Parameter|null $parent Parent container of the parameter + * + * @return self + */ + public function setParent($parent) + { + $this->parent = $parent; + + return $this; + } + + /** + * Get the properties of the parameter + * + * @return array + */ + public function getProperties() + { + if (!$this->propertiesCache) { + $this->propertiesCache = array(); + foreach (array_keys($this->properties) as $name) { + $this->propertiesCache[$name] = $this->getProperty($name); + } + } + + return $this->propertiesCache; + } + + /** + * Get a specific property from the parameter + * + * @param string $name Name of the property to retrieve + * + * @return null|Parameter + */ + public function getProperty($name) + { + if (!isset($this->properties[$name])) { + return null; + } + + if (!($this->properties[$name] instanceof self)) { + $this->properties[$name]['name'] = $name; + $this->properties[$name] = new static($this->properties[$name], $this->serviceDescription); + $this->properties[$name]->setParent($this); + } + + return $this->properties[$name]; + } + + /** + * Remove a property from the parameter + * + * @param string $name Name of the property to remove + * + * @return self + */ + public function removeProperty($name) + { + unset($this->properties[$name]); + $this->propertiesCache = null; + + return $this; + } + + /** + * Add a property to the parameter + * + * @param Parameter $property Properties to set + * + * @return self + */ + public function addProperty(Parameter $property) + { + $this->properties[$property->getName()] = $property; + $property->setParent($this); + $this->propertiesCache = null; + + return $this; + } + + /** + * Get the additionalProperties value of the parameter + * + * @return bool|Parameter|null + */ + public function getAdditionalProperties() + { + if (is_array($this->additionalProperties)) { + $this->additionalProperties = new static($this->additionalProperties, $this->serviceDescription); + $this->additionalProperties->setParent($this); + } + + return $this->additionalProperties; + } + + /** + * Set the additionalProperties value of the parameter + * + * @param bool|Parameter|null $additional Boolean to allow any, an Parameter to specify a schema, or false to disallow + * + * @return self + */ + public function setAdditionalProperties($additional) + { + $this->additionalProperties = $additional; + + return $this; + } + + /** + * Set the items data of the parameter + * + * @param Parameter|null $items Items to set + * + * @return self + */ + public function setItems(Parameter $items = null) + { + if ($this->items = $items) { + $this->items->setParent($this); + } + + return $this; + } + + /** + * Get the item data of the parameter + * + * @return Parameter|null + */ + public function getItems() + { + if (is_array($this->items)) { + $this->items = new static($this->items, $this->serviceDescription); + $this->items->setParent($this); + } + + return $this->items; + } + + /** + * Get the class that the parameter must implement + * + * @return null|string + */ + public function getInstanceOf() + { + return $this->instanceOf; + } + + /** + * Set the class that the parameter must be an instance of + * + * @param string|null $instanceOf Class or interface name + * + * @return self + */ + public function setInstanceOf($instanceOf) + { + $this->instanceOf = $instanceOf; + + return $this; + } + + /** + * Get the enum of strings that are valid for the parameter + * + * @return array|null + */ + public function getEnum() + { + return $this->enum; + } + + /** + * Set the enum of strings that are valid for the parameter + * + * @param array|null $enum Array of strings or null + * + * @return self + */ + public function setEnum(array $enum = null) + { + $this->enum = $enum; + + return $this; + } + + /** + * Get the regex pattern that must match a value when the value is a string + * + * @return string + */ + public function getPattern() + { + return $this->pattern; + } + + /** + * Set the regex pattern that must match a value when the value is a string + * + * @param string $pattern Regex pattern + * + * @return self + */ + public function setPattern($pattern) + { + $this->pattern = $pattern; + + return $this; + } + + /** + * Get the format attribute of the schema + * + * @return string + */ + public function getFormat() + { + return $this->format; + } + + /** + * Set the format attribute of the schema + * + * @param string $format Format to set (e.g. date, date-time, timestamp, time, date-time-http) + * + * @return self + */ + public function setFormat($format) + { + $this->format = $format; + + return $this; + } +} diff --git a/guzzle/guzzle/src/Guzzle/Service/Description/SchemaFormatter.php b/guzzle/guzzle/src/Guzzle/Service/Description/SchemaFormatter.php new file mode 100644 index 000000000..7f47fc9d7 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Service/Description/SchemaFormatter.php @@ -0,0 +1,156 @@ +setTimezone(self::getUtcTimeZone())->format($format); + } + + throw new InvalidArgumentException('Date/Time values must be either a string, integer, or DateTime object'); + } +} diff --git a/guzzle/guzzle/src/Guzzle/Service/Description/SchemaValidator.php b/guzzle/guzzle/src/Guzzle/Service/Description/SchemaValidator.php new file mode 100644 index 000000000..13b99cdf2 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Service/Description/SchemaValidator.php @@ -0,0 +1,290 @@ +castIntegerToStringType = $castIntegerToStringType; + } + + public function validate(Parameter $param, &$value) + { + $this->errors = array(); + $this->recursiveProcess($param, $value); + + if (empty($this->errors)) { + return true; + } else { + sort($this->errors); + return false; + } + } + + /** + * Get the errors encountered while validating + * + * @return array + */ + public function getErrors() + { + return $this->errors ?: array(); + } + + /** + * Recursively validate a parameter + * + * @param Parameter $param API parameter being validated + * @param mixed $value Value to validate and validate. The value may change during this validate. + * @param string $path Current validation path (used for error reporting) + * @param int $depth Current depth in the validation validate + * + * @return bool Returns true if valid, or false if invalid + */ + protected function recursiveProcess(Parameter $param, &$value, $path = '', $depth = 0) + { + // Update the value by adding default or static values + $value = $param->getValue($value); + + $required = $param->getRequired(); + // if the value is null and the parameter is not required or is static, then skip any further recursion + if ((null === $value && !$required) || $param->getStatic()) { + return true; + } + + $type = $param->getType(); + // Attempt to limit the number of times is_array is called by tracking if the value is an array + $valueIsArray = is_array($value); + // If a name is set then update the path so that validation messages are more helpful + if ($name = $param->getName()) { + $path .= "[{$name}]"; + } + + if ($type == 'object') { + + // Objects are either associative arrays, ToArrayInterface, or some other object + if ($param->getInstanceOf()) { + $instance = $param->getInstanceOf(); + if (!($value instanceof $instance)) { + $this->errors[] = "{$path} must be an instance of {$instance}"; + return false; + } + } + + // Determine whether or not this "value" has properties and should be traversed + $traverse = $temporaryValue = false; + + // Convert the value to an array + if (!$valueIsArray && $value instanceof ToArrayInterface) { + $value = $value->toArray(); + } + + if ($valueIsArray) { + // Ensure that the array is associative and not numerically indexed + if (isset($value[0])) { + $this->errors[] = "{$path} must be an array of properties. Got a numerically indexed array."; + return false; + } + $traverse = true; + } elseif ($value === null) { + // Attempt to let the contents be built up by default values if possible + $value = array(); + $temporaryValue = $valueIsArray = $traverse = true; + } + + if ($traverse) { + + if ($properties = $param->getProperties()) { + // if properties were found, the validate each property of the value + foreach ($properties as $property) { + $name = $property->getName(); + if (isset($value[$name])) { + $this->recursiveProcess($property, $value[$name], $path, $depth + 1); + } else { + $current = null; + $this->recursiveProcess($property, $current, $path, $depth + 1); + // Only set the value if it was populated with something + if (null !== $current) { + $value[$name] = $current; + } + } + } + } + + $additional = $param->getAdditionalProperties(); + if ($additional !== true) { + // If additional properties were found, then validate each against the additionalProperties attr. + $keys = array_keys($value); + // Determine the keys that were specified that were not listed in the properties of the schema + $diff = array_diff($keys, array_keys($properties)); + if (!empty($diff)) { + // Determine which keys are not in the properties + if ($additional instanceOf Parameter) { + foreach ($diff as $key) { + $this->recursiveProcess($additional, $value[$key], "{$path}[{$key}]", $depth); + } + } else { + // if additionalProperties is set to false and there are additionalProperties in the values, then fail + $keys = array_keys($value); + $this->errors[] = sprintf('%s[%s] is not an allowed property', $path, reset($keys)); + } + } + } + + // A temporary value will be used to traverse elements that have no corresponding input value. + // This allows nested required parameters with default values to bubble up into the input. + // Here we check if we used a temp value and nothing bubbled up, then we need to remote the value. + if ($temporaryValue && empty($value)) { + $value = null; + $valueIsArray = false; + } + } + + } elseif ($type == 'array' && $valueIsArray && $param->getItems()) { + foreach ($value as $i => &$item) { + // Validate each item in an array against the items attribute of the schema + $this->recursiveProcess($param->getItems(), $item, $path . "[{$i}]", $depth + 1); + } + } + + // If the value is required and the type is not null, then there is an error if the value is not set + if ($required && $value === null && $type != 'null') { + $message = "{$path} is " . ($param->getType() ? ('a required ' . implode(' or ', (array) $param->getType())) : 'required'); + if ($param->getDescription()) { + $message .= ': ' . $param->getDescription(); + } + $this->errors[] = $message; + return false; + } + + // Validate that the type is correct. If the type is string but an integer was passed, the class can be + // instructed to cast the integer to a string to pass validation. This is the default behavior. + if ($type && (!$type = $this->determineType($type, $value))) { + if ($this->castIntegerToStringType && $param->getType() == 'string' && is_integer($value)) { + $value = (string) $value; + } else { + $this->errors[] = "{$path} must be of type " . implode(' or ', (array) $param->getType()); + } + } + + // Perform type specific validation for strings, arrays, and integers + if ($type == 'string') { + + // Strings can have enums which are a list of predefined values + if (($enum = $param->getEnum()) && !in_array($value, $enum)) { + $this->errors[] = "{$path} must be one of " . implode(' or ', array_map(function ($s) { + return '"' . addslashes($s) . '"'; + }, $enum)); + } + // Strings can have a regex pattern that the value must match + if (($pattern = $param->getPattern()) && !preg_match($pattern, $value)) { + $this->errors[] = "{$path} must match the following regular expression: {$pattern}"; + } + + $strLen = null; + if ($min = $param->getMinLength()) { + $strLen = strlen($value); + if ($strLen < $min) { + $this->errors[] = "{$path} length must be greater than or equal to {$min}"; + } + } + if ($max = $param->getMaxLength()) { + if (($strLen ?: strlen($value)) > $max) { + $this->errors[] = "{$path} length must be less than or equal to {$max}"; + } + } + + } elseif ($type == 'array') { + + $size = null; + if ($min = $param->getMinItems()) { + $size = count($value); + if ($size < $min) { + $this->errors[] = "{$path} must contain {$min} or more elements"; + } + } + if ($max = $param->getMaxItems()) { + if (($size ?: count($value)) > $max) { + $this->errors[] = "{$path} must contain {$max} or fewer elements"; + } + } + + } elseif ($type == 'integer' || $type == 'number' || $type == 'numeric') { + if (($min = $param->getMinimum()) && $value < $min) { + $this->errors[] = "{$path} must be greater than or equal to {$min}"; + } + if (($max = $param->getMaximum()) && $value > $max) { + $this->errors[] = "{$path} must be less than or equal to {$max}"; + } + } + + return empty($this->errors); + } + + /** + * From the allowable types, determine the type that the variable matches + * + * @param string $type Parameter type + * @param mixed $value Value to determine the type + * + * @return string|bool Returns the matching type on + */ + protected function determineType($type, $value) + { + foreach ((array) $type as $t) { + if ($t == 'string' && (is_string($value) || (is_object($value) && method_exists($value, '__toString')))) { + return 'string'; + } elseif ($t == 'object' && (is_array($value) || is_object($value))) { + return 'object'; + } elseif ($t == 'array' && is_array($value)) { + return 'array'; + } elseif ($t == 'integer' && is_integer($value)) { + return 'integer'; + } elseif ($t == 'boolean' && is_bool($value)) { + return 'boolean'; + } elseif ($t == 'number' && is_numeric($value)) { + return 'number'; + } elseif ($t == 'numeric' && is_numeric($value)) { + return 'numeric'; + } elseif ($t == 'null' && !$value) { + return 'null'; + } elseif ($t == 'any') { + return 'any'; + } + } + + return false; + } +} diff --git a/guzzle/guzzle/src/Guzzle/Service/Description/ServiceDescription.php b/guzzle/guzzle/src/Guzzle/Service/Description/ServiceDescription.php new file mode 100644 index 000000000..286e65eec --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Service/Description/ServiceDescription.php @@ -0,0 +1,271 @@ +load($config, $options); + } + + /** + * @param array $config Array of configuration data + */ + public function __construct(array $config = array()) + { + $this->fromArray($config); + } + + public function serialize() + { + return json_encode($this->toArray()); + } + + public function unserialize($json) + { + $this->operations = array(); + $this->fromArray(json_decode($json, true)); + } + + public function toArray() + { + $result = array( + 'name' => $this->name, + 'apiVersion' => $this->apiVersion, + 'baseUrl' => $this->baseUrl, + 'description' => $this->description + ) + $this->extraData; + $result['operations'] = array(); + foreach ($this->getOperations() as $name => $operation) { + $result['operations'][$operation->getName() ?: $name] = $operation->toArray(); + } + if (!empty($this->models)) { + $result['models'] = array(); + foreach ($this->models as $id => $model) { + $result['models'][$id] = $model instanceof Parameter ? $model->toArray(): $model; + } + } + + return array_filter($result); + } + + public function getBaseUrl() + { + return $this->baseUrl; + } + + /** + * Set the baseUrl of the description + * + * @param string $baseUrl Base URL of each operation + * + * @return self + */ + public function setBaseUrl($baseUrl) + { + $this->baseUrl = $baseUrl; + + return $this; + } + + public function getOperations() + { + foreach (array_keys($this->operations) as $name) { + $this->getOperation($name); + } + + return $this->operations; + } + + public function hasOperation($name) + { + return isset($this->operations[$name]); + } + + public function getOperation($name) + { + // Lazily retrieve and build operations + if (!isset($this->operations[$name])) { + return null; + } + + if (!($this->operations[$name] instanceof Operation)) { + $this->operations[$name] = new Operation($this->operations[$name], $this); + } + + return $this->operations[$name]; + } + + /** + * Add a operation to the service description + * + * @param OperationInterface $operation Operation to add + * + * @return self + */ + public function addOperation(OperationInterface $operation) + { + $this->operations[$operation->getName()] = $operation->setServiceDescription($this); + + return $this; + } + + public function getModel($id) + { + if (!isset($this->models[$id])) { + return null; + } + + if (!($this->models[$id] instanceof Parameter)) { + $this->models[$id] = new Parameter($this->models[$id] + array('name' => $id), $this); + } + + return $this->models[$id]; + } + + public function getModels() + { + // Ensure all models are converted into parameter objects + foreach (array_keys($this->models) as $id) { + $this->getModel($id); + } + + return $this->models; + } + + public function hasModel($id) + { + return isset($this->models[$id]); + } + + /** + * Add a model to the service description + * + * @param Parameter $model Model to add + * + * @return self + */ + public function addModel(Parameter $model) + { + $this->models[$model->getName()] = $model; + + return $this; + } + + public function getApiVersion() + { + return $this->apiVersion; + } + + public function getName() + { + return $this->name; + } + + public function getDescription() + { + return $this->description; + } + + public function getData($key) + { + return isset($this->extraData[$key]) ? $this->extraData[$key] : null; + } + + public function setData($key, $value) + { + $this->extraData[$key] = $value; + + return $this; + } + + /** + * Initialize the state from an array + * + * @param array $config Configuration data + * @throws InvalidArgumentException + */ + protected function fromArray(array $config) + { + // Keep a list of default keys used in service descriptions that is later used to determine extra data keys + static $defaultKeys = array('name', 'models', 'apiVersion', 'baseUrl', 'description'); + // Pull in the default configuration values + foreach ($defaultKeys as $key) { + if (isset($config[$key])) { + $this->{$key} = $config[$key]; + } + } + + // Account for the Swagger name for Guzzle's baseUrl + if (isset($config['basePath'])) { + $this->baseUrl = $config['basePath']; + } + + // Ensure that the models and operations properties are always arrays + $this->models = (array) $this->models; + $this->operations = (array) $this->operations; + + // We want to add operations differently than adding the other properties + $defaultKeys[] = 'operations'; + + // Create operations for each operation + if (isset($config['operations'])) { + foreach ($config['operations'] as $name => $operation) { + if (!($operation instanceof Operation) && !is_array($operation)) { + throw new InvalidArgumentException('Invalid operation in service description: ' + . gettype($operation)); + } + $this->operations[$name] = $operation; + } + } + + // Get all of the additional properties of the service description and store them in a data array + foreach (array_diff(array_keys($config), $defaultKeys) as $key) { + $this->extraData[$key] = $config[$key]; + } + } +} diff --git a/guzzle/guzzle/src/Guzzle/Service/Description/ServiceDescriptionInterface.php b/guzzle/guzzle/src/Guzzle/Service/Description/ServiceDescriptionInterface.php new file mode 100644 index 000000000..5983e586b --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Service/Description/ServiceDescriptionInterface.php @@ -0,0 +1,106 @@ + $op) { + $name = $op['name'] = isset($op['name']) ? $op['name'] : $name; + // Extend other operations + if (!empty($op['extends'])) { + $this->resolveExtension($name, $op, $operations); + } + $op['parameters'] = isset($op['parameters']) ? $op['parameters'] : array(); + $operations[$name] = $op; + } + } + + return new ServiceDescription(array( + 'apiVersion' => isset($config['apiVersion']) ? $config['apiVersion'] : null, + 'baseUrl' => isset($config['baseUrl']) ? $config['baseUrl'] : null, + 'description' => isset($config['description']) ? $config['description'] : null, + 'operations' => $operations, + 'models' => isset($config['models']) ? $config['models'] : null + ) + $config); + } + + /** + * @param string $name Name of the operation + * @param array $op Operation value array + * @param array $operations Currently loaded operations + * @throws DescriptionBuilderException when extending a non-existent operation + */ + protected function resolveExtension($name, array &$op, array &$operations) + { + $resolved = array(); + $original = empty($op['parameters']) ? false: $op['parameters']; + $hasClass = !empty($op['class']); + foreach ((array) $op['extends'] as $extendedCommand) { + if (empty($operations[$extendedCommand])) { + throw new DescriptionBuilderException("{$name} extends missing operation {$extendedCommand}"); + } + $toArray = $operations[$extendedCommand]; + $resolved = empty($resolved) + ? $toArray['parameters'] + : array_merge($resolved, $toArray['parameters']); + + $op = $op + $toArray; + if (!$hasClass && isset($toArray['class'])) { + $op['class'] = $toArray['class']; + } + } + $op['parameters'] = $original ? array_merge($resolved, $original) : $resolved; + } +} diff --git a/guzzle/guzzle/src/Guzzle/Service/Description/ValidatorInterface.php b/guzzle/guzzle/src/Guzzle/Service/Description/ValidatorInterface.php new file mode 100644 index 000000000..94ca77da4 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Service/Description/ValidatorInterface.php @@ -0,0 +1,28 @@ +getMessage(), $e->getCode(), $e->getPrevious()); + $ce->setSuccessfulRequests($e->getSuccessfulRequests()); + + $alreadyAddedExceptions = array(); + foreach ($e->getFailedRequests() as $request) { + if ($re = $e->getExceptionForFailedRequest($request)) { + $alreadyAddedExceptions[] = $re; + $ce->addFailedRequestWithException($request, $re); + } else { + $ce->addFailedRequest($request); + } + } + + // Add any exceptions that did not map to a request + if (count($alreadyAddedExceptions) < count($e)) { + foreach ($e as $ex) { + if (!in_array($ex, $alreadyAddedExceptions)) { + $ce->add($ex); + } + } + } + + return $ce; + } + + /** + * Get all of the commands in the transfer + * + * @return array + */ + public function getAllCommands() + { + return array_merge($this->successfulCommands, $this->failedCommands); + } + + /** + * Add to the array of successful commands + * + * @param CommandInterface $command Successful command + * + * @return self + */ + public function addSuccessfulCommand(CommandInterface $command) + { + $this->successfulCommands[] = $command; + + return $this; + } + + /** + * Add to the array of failed commands + * + * @param CommandInterface $command Failed command + * + * @return self + */ + public function addFailedCommand(CommandInterface $command) + { + $this->failedCommands[] = $command; + + return $this; + } + + /** + * Get an array of successful commands + * + * @return array + */ + public function getSuccessfulCommands() + { + return $this->successfulCommands; + } + + /** + * Get an array of failed commands + * + * @return array + */ + public function getFailedCommands() + { + return $this->failedCommands; + } + + /** + * Get the Exception that caused the given $command to fail + * + * @param CommandInterface $command Failed command + * + * @return \Exception|null + */ + public function getExceptionForFailedCommand(CommandInterface $command) + { + return $this->getExceptionForFailedRequest($command->getRequest()); + } +} diff --git a/guzzle/guzzle/src/Guzzle/Service/Exception/DescriptionBuilderException.php b/guzzle/guzzle/src/Guzzle/Service/Exception/DescriptionBuilderException.php new file mode 100644 index 000000000..1407e5687 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Service/Exception/DescriptionBuilderException.php @@ -0,0 +1,7 @@ +invalidCommands = $commands; + parent::__construct( + 'Encountered commands in a batch transfer that use inconsistent clients. The batching ' . + 'strategy you use with a command transfer must divide command batches by client.' + ); + } + + /** + * Get the invalid commands + * + * @return array + */ + public function getCommands() + { + return $this->invalidCommands; + } +} diff --git a/guzzle/guzzle/src/Guzzle/Service/Exception/ResponseClassException.php b/guzzle/guzzle/src/Guzzle/Service/Exception/ResponseClassException.php new file mode 100644 index 000000000..d59ff2185 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Service/Exception/ResponseClassException.php @@ -0,0 +1,9 @@ +errors = $errors; + } + + /** + * Get any validation errors + * + * @return array + */ + public function getErrors() + { + return $this->errors; + } +} diff --git a/guzzle/guzzle/src/Guzzle/Service/Resource/AbstractResourceIteratorFactory.php b/guzzle/guzzle/src/Guzzle/Service/Resource/AbstractResourceIteratorFactory.php new file mode 100644 index 000000000..21140e772 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Service/Resource/AbstractResourceIteratorFactory.php @@ -0,0 +1,37 @@ +canBuild($command)) { + throw new InvalidArgumentException('Iterator was not found for ' . $command->getName()); + } + + $className = $this->getClassName($command); + + return new $className($command, $options); + } + + public function canBuild(CommandInterface $command) + { + return (bool) $this->getClassName($command); + } + + /** + * Get the name of the class to instantiate for the command + * + * @param CommandInterface $command Command that is associated with the iterator + * + * @return string + */ + abstract protected function getClassName(CommandInterface $command); +} diff --git a/guzzle/guzzle/src/Guzzle/Service/Resource/CompositeResourceIteratorFactory.php b/guzzle/guzzle/src/Guzzle/Service/Resource/CompositeResourceIteratorFactory.php new file mode 100644 index 000000000..2efc133c6 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Service/Resource/CompositeResourceIteratorFactory.php @@ -0,0 +1,67 @@ +factories = $factories; + } + + public function build(CommandInterface $command, array $options = array()) + { + if (!($factory = $this->getFactory($command))) { + throw new InvalidArgumentException('Iterator was not found for ' . $command->getName()); + } + + return $factory->build($command, $options); + } + + public function canBuild(CommandInterface $command) + { + return $this->getFactory($command) !== false; + } + + /** + * Add a factory to the composite factory + * + * @param ResourceIteratorFactoryInterface $factory Factory to add + * + * @return self + */ + public function addFactory(ResourceIteratorFactoryInterface $factory) + { + $this->factories[] = $factory; + + return $this; + } + + /** + * Get the factory that matches the command object + * + * @param CommandInterface $command Command retrieving the iterator for + * + * @return ResourceIteratorFactoryInterface|bool + */ + protected function getFactory(CommandInterface $command) + { + foreach ($this->factories as $factory) { + if ($factory->canBuild($command)) { + return $factory; + } + } + + return false; + } +} diff --git a/guzzle/guzzle/src/Guzzle/Service/Resource/MapResourceIteratorFactory.php b/guzzle/guzzle/src/Guzzle/Service/Resource/MapResourceIteratorFactory.php new file mode 100644 index 000000000..c71ca9d85 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Service/Resource/MapResourceIteratorFactory.php @@ -0,0 +1,34 @@ +map = $map; + } + + public function getClassName(CommandInterface $command) + { + $className = $command->getName(); + + if (isset($this->map[$className])) { + return $this->map[$className]; + } elseif (isset($this->map['*'])) { + // If a wildcard was added, then always use that + return $this->map['*']; + } + + return null; + } +} diff --git a/guzzle/guzzle/src/Guzzle/Service/Resource/Model.php b/guzzle/guzzle/src/Guzzle/Service/Resource/Model.php new file mode 100644 index 000000000..2322434a5 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Service/Resource/Model.php @@ -0,0 +1,64 @@ +data = $data; + $this->structure = $structure; + } + + /** + * Get the structure of the model + * + * @return Parameter + */ + public function getStructure() + { + return $this->structure ?: new Parameter(); + } + + /** + * Provides debug information about the model object + * + * @return string + */ + public function __toString() + { + $output = 'Debug output of '; + if ($this->structure) { + $output .= $this->structure->getName() . ' '; + } + $output .= 'model'; + $output = str_repeat('=', strlen($output)) . "\n" . $output . "\n" . str_repeat('=', strlen($output)) . "\n\n"; + $output .= "Model data\n-----------\n\n"; + $output .= "This data can be retrieved from the model object using the get() method of the model " + . "(e.g. \$model->get(\$key)) or accessing the model like an associative array (e.g. \$model['key']).\n\n"; + $lines = array_slice(explode("\n", trim(print_r($this->toArray(), true))), 2, -1); + $output .= implode("\n", $lines); + + if ($this->structure) { + $output .= "\n\nModel structure\n---------------\n\n"; + $output .= "The following JSON document defines how the model was parsed from an HTTP response into the " + . "associative array structure you see above.\n\n"; + $output .= ' ' . json_encode($this->structure->toArray()) . "\n\n"; + } + + return $output . "\n"; + } +} diff --git a/guzzle/guzzle/src/Guzzle/Service/Resource/ResourceIterator.php b/guzzle/guzzle/src/Guzzle/Service/Resource/ResourceIterator.php new file mode 100644 index 000000000..e14152432 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Service/Resource/ResourceIterator.php @@ -0,0 +1,254 @@ +originalCommand = $command; + + // Parse options from the array of options + $this->data = $data; + $this->limit = array_key_exists('limit', $data) ? $data['limit'] : 0; + $this->pageSize = array_key_exists('page_size', $data) ? $data['page_size'] : false; + } + + /** + * Get all of the resources as an array (Warning: this could issue a large number of requests) + * + * @return array + */ + public function toArray() + { + return iterator_to_array($this, false); + } + + public function setLimit($limit) + { + $this->limit = $limit; + $this->resetState(); + + return $this; + } + + public function setPageSize($pageSize) + { + $this->pageSize = $pageSize; + $this->resetState(); + + return $this; + } + + /** + * Get an option from the iterator + * + * @param string $key Key of the option to retrieve + * + * @return mixed|null Returns NULL if not set or the value if set + */ + public function get($key) + { + return array_key_exists($key, $this->data) ? $this->data[$key] : null; + } + + /** + * Set an option on the iterator + * + * @param string $key Key of the option to set + * @param mixed $value Value to set for the option + * + * @return ResourceIterator + */ + public function set($key, $value) + { + $this->data[$key] = $value; + + return $this; + } + + public function current() + { + return $this->resources ? current($this->resources) : false; + } + + public function key() + { + return max(0, $this->iteratedCount - 1); + } + + public function count() + { + return $this->retrievedCount; + } + + /** + * Get the total number of requests sent + * + * @return int + */ + public function getRequestCount() + { + return $this->requestCount; + } + + /** + * Rewind the Iterator to the first element and send the original command + */ + public function rewind() + { + // Use the original command + $this->command = clone $this->originalCommand; + $this->resetState(); + $this->next(); + } + + public function valid() + { + return !$this->invalid && (!$this->resources || $this->current() || $this->nextToken) + && (!$this->limit || $this->iteratedCount < $this->limit + 1); + } + + public function next() + { + $this->iteratedCount++; + + // Check if a new set of resources needs to be retrieved + $sendRequest = false; + if (!$this->resources) { + $sendRequest = true; + } else { + // iterate over the internal array + $current = next($this->resources); + $sendRequest = $current === false && $this->nextToken && (!$this->limit || $this->iteratedCount < $this->limit + 1); + } + + if ($sendRequest) { + + $this->dispatch('resource_iterator.before_send', array( + 'iterator' => $this, + 'resources' => $this->resources + )); + + // Get a new command object from the original command + $this->command = clone $this->originalCommand; + // Send a request and retrieve the newly loaded resources + $this->resources = $this->sendRequest(); + $this->requestCount++; + + // If no resources were found, then the last request was not needed + // and iteration must stop + if (empty($this->resources)) { + $this->invalid = true; + } else { + // Add to the number of retrieved resources + $this->retrievedCount += count($this->resources); + // Ensure that we rewind to the beginning of the array + reset($this->resources); + } + + $this->dispatch('resource_iterator.after_send', array( + 'iterator' => $this, + 'resources' => $this->resources + )); + } + } + + /** + * Retrieve the NextToken that can be used in other iterators. + * + * @return string Returns a NextToken + */ + public function getNextToken() + { + return $this->nextToken; + } + + /** + * Returns the value that should be specified for the page size for a request that will maintain any hard limits, + * but still honor the specified pageSize if the number of items retrieved + pageSize < hard limit + * + * @return int Returns the page size of the next request. + */ + protected function calculatePageSize() + { + if ($this->limit && $this->iteratedCount + $this->pageSize > $this->limit) { + return 1 + ($this->limit - $this->iteratedCount); + } + + return (int) $this->pageSize; + } + + /** + * Reset the internal state of the iterator without triggering a rewind() + */ + protected function resetState() + { + $this->iteratedCount = 0; + $this->retrievedCount = 0; + $this->nextToken = false; + $this->resources = null; + $this->invalid = false; + } + + /** + * Send a request to retrieve the next page of results. Hook for subclasses to implement. + * + * @return array Returns the newly loaded resources + */ + abstract protected function sendRequest(); +} diff --git a/guzzle/guzzle/src/Guzzle/Service/Resource/ResourceIteratorApplyBatched.php b/guzzle/guzzle/src/Guzzle/Service/Resource/ResourceIteratorApplyBatched.php new file mode 100644 index 000000000..6aa36153f --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Service/Resource/ResourceIteratorApplyBatched.php @@ -0,0 +1,111 @@ +iterator = $iterator; + $this->callback = $callback; + Version::warn(__CLASS__ . ' is deprecated'); + } + + /** + * Apply the callback to the contents of the resource iterator + * + * @param int $perBatch The number of records to group per batch transfer + * + * @return int Returns the number of iterated resources + */ + public function apply($perBatch = 50) + { + $this->iterated = $this->batches = $batches = 0; + $that = $this; + $it = $this->iterator; + $callback = $this->callback; + + $batch = BatchBuilder::factory() + ->createBatchesWith(new BatchSizeDivisor($perBatch)) + ->transferWith(new BatchClosureTransfer(function (array $batch) use ($that, $callback, &$batches, $it) { + $batches++; + $that->dispatch('iterator_batch.before_batch', array('iterator' => $it, 'batch' => $batch)); + call_user_func_array($callback, array($it, $batch)); + $that->dispatch('iterator_batch.after_batch', array('iterator' => $it, 'batch' => $batch)); + })) + ->autoFlushAt($perBatch) + ->build(); + + $this->dispatch('iterator_batch.created_batch', array('batch' => $batch)); + + foreach ($this->iterator as $resource) { + $this->iterated++; + $batch->add($resource); + } + + $batch->flush(); + $this->batches = $batches; + + return $this->iterated; + } + + /** + * Get the total number of batches sent + * + * @return int + */ + public function getBatchCount() + { + return $this->batches; + } + + /** + * Get the total number of iterated resources + * + * @return int + */ + public function getIteratedCount() + { + return $this->iterated; + } +} diff --git a/guzzle/guzzle/src/Guzzle/Service/Resource/ResourceIteratorClassFactory.php b/guzzle/guzzle/src/Guzzle/Service/Resource/ResourceIteratorClassFactory.php new file mode 100644 index 000000000..2fd998071 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Service/Resource/ResourceIteratorClassFactory.php @@ -0,0 +1,60 @@ + AbcFoo). + */ +class ResourceIteratorClassFactory extends AbstractResourceIteratorFactory +{ + /** @var array List of namespaces used to look for classes */ + protected $namespaces; + + /** @var InflectorInterface Inflector used to determine class names */ + protected $inflector; + + /** + * @param string|array $namespaces List of namespaces for iterator objects + * @param InflectorInterface $inflector Inflector used to resolve class names + */ + public function __construct($namespaces = array(), InflectorInterface $inflector = null) + { + $this->namespaces = (array) $namespaces; + $this->inflector = $inflector ?: Inflector::getDefault(); + } + + /** + * Registers a namespace to check for Iterators + * + * @param string $namespace Namespace which contains Iterator classes + * + * @return self + */ + public function registerNamespace($namespace) + { + array_unshift($this->namespaces, $namespace); + + return $this; + } + + protected function getClassName(CommandInterface $command) + { + $iteratorName = $this->inflector->camel($command->getName()) . 'Iterator'; + + // Determine the name of the class to load + foreach ($this->namespaces as $namespace) { + $potentialClassName = $namespace . '\\' . $iteratorName; + if (class_exists($potentialClassName)) { + return $potentialClassName; + } + } + + return false; + } +} diff --git a/guzzle/guzzle/src/Guzzle/Service/Resource/ResourceIteratorFactoryInterface.php b/guzzle/guzzle/src/Guzzle/Service/Resource/ResourceIteratorFactoryInterface.php new file mode 100644 index 000000000..8b4e8dbe0 --- /dev/null +++ b/guzzle/guzzle/src/Guzzle/Service/Resource/ResourceIteratorFactoryInterface.php @@ -0,0 +1,30 @@ +=5.3.2", + "guzzle/cache": "self.version", + "guzzle/http": "self.version", + "guzzle/inflection": "self.version" + }, + "autoload": { + "psr-0": { "Guzzle\\Service": "" } + }, + "target-dir": "Guzzle/Service", + "extra": { + "branch-alias": { + "dev-master": "3.7-dev" + } + } +} diff --git a/guzzle/stream/Guzzle/Stream/PhpStreamRequestFactory.php b/guzzle/guzzle/src/Guzzle/Stream/PhpStreamRequestFactory.php similarity index 100% rename from guzzle/stream/Guzzle/Stream/PhpStreamRequestFactory.php rename to guzzle/guzzle/src/Guzzle/Stream/PhpStreamRequestFactory.php diff --git a/guzzle/stream/Guzzle/Stream/Stream.php b/guzzle/guzzle/src/Guzzle/Stream/Stream.php similarity index 100% rename from guzzle/stream/Guzzle/Stream/Stream.php rename to guzzle/guzzle/src/Guzzle/Stream/Stream.php diff --git a/guzzle/stream/Guzzle/Stream/StreamInterface.php b/guzzle/guzzle/src/Guzzle/Stream/StreamInterface.php similarity index 100% rename from guzzle/stream/Guzzle/Stream/StreamInterface.php rename to guzzle/guzzle/src/Guzzle/Stream/StreamInterface.php diff --git a/guzzle/stream/Guzzle/Stream/StreamRequestFactoryInterface.php b/guzzle/guzzle/src/Guzzle/Stream/StreamRequestFactoryInterface.php similarity index 100% rename from guzzle/stream/Guzzle/Stream/StreamRequestFactoryInterface.php rename to guzzle/guzzle/src/Guzzle/Stream/StreamRequestFactoryInterface.php diff --git a/guzzle/stream/Guzzle/Stream/composer.json b/guzzle/guzzle/src/Guzzle/Stream/composer.json similarity index 100% rename from guzzle/stream/Guzzle/Stream/composer.json rename to guzzle/guzzle/src/Guzzle/Stream/composer.json diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Batch/AbstractBatchDecoratorTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Batch/AbstractBatchDecoratorTest.php new file mode 100644 index 000000000..951738d2a --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Batch/AbstractBatchDecoratorTest.php @@ -0,0 +1,33 @@ +getMock('Guzzle\Batch\BatchTransferInterface'), + $this->getMock('Guzzle\Batch\BatchDivisorInterface') + ); + + $decoratorA = $this->getMockBuilder('Guzzle\Batch\AbstractBatchDecorator') + ->setConstructorArgs(array($batch)) + ->getMockForAbstractClass(); + + $decoratorB = $this->getMockBuilder('Guzzle\Batch\AbstractBatchDecorator') + ->setConstructorArgs(array($decoratorA)) + ->getMockForAbstractClass(); + + $decoratorA->add('foo'); + $this->assertFalse($decoratorB->isEmpty()); + $this->assertFalse($batch->isEmpty()); + $this->assertEquals(array($decoratorB, $decoratorA), $decoratorB->getDecorators()); + $this->assertEquals(array(), $decoratorB->flush()); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Batch/BatchBuilderTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Batch/BatchBuilderTest.php new file mode 100644 index 000000000..4da09d30e --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Batch/BatchBuilderTest.php @@ -0,0 +1,86 @@ +getMock('Guzzle\Batch\BatchTransferInterface'); + } + + private function getMockDivisor() + { + return $this->getMock('Guzzle\Batch\BatchDivisorInterface'); + } + + private function getMockBatchBuilder() + { + return BatchBuilder::factory() + ->transferWith($this->getMockTransfer()) + ->createBatchesWith($this->getMockDivisor()); + } + + public function testFactoryCreatesInstance() + { + $builder = BatchBuilder::factory(); + $this->assertInstanceOf('Guzzle\Batch\BatchBuilder', $builder); + } + + public function testAddsAutoFlush() + { + $batch = $this->getMockBatchBuilder()->autoFlushAt(10)->build(); + $this->assertInstanceOf('Guzzle\Batch\FlushingBatch', $batch); + } + + public function testAddsExceptionBuffering() + { + $batch = $this->getMockBatchBuilder()->bufferExceptions()->build(); + $this->assertInstanceOf('Guzzle\Batch\ExceptionBufferingBatch', $batch); + } + + public function testAddHistory() + { + $batch = $this->getMockBatchBuilder()->keepHistory()->build(); + $this->assertInstanceOf('Guzzle\Batch\HistoryBatch', $batch); + } + + public function testAddsNotify() + { + $batch = $this->getMockBatchBuilder()->notify(function() {})->build(); + $this->assertInstanceOf('Guzzle\Batch\NotifyingBatch', $batch); + } + + /** + * @expectedException Guzzle\Common\Exception\RuntimeException + */ + public function testTransferStrategyMustBeSet() + { + $batch = BatchBuilder::factory()->createBatchesWith($this->getMockDivisor())->build(); + } + + /** + * @expectedException Guzzle\Common\Exception\RuntimeException + */ + public function testDivisorStrategyMustBeSet() + { + $batch = BatchBuilder::factory()->transferWith($this->getMockTransfer())->build(); + } + + public function testTransfersRequests() + { + $batch = BatchBuilder::factory()->transferRequests(10)->build(); + $this->assertInstanceOf('Guzzle\Batch\BatchRequestTransfer', $this->readAttribute($batch, 'transferStrategy')); + } + + public function testTransfersCommands() + { + $batch = BatchBuilder::factory()->transferCommands(10)->build(); + $this->assertInstanceOf('Guzzle\Batch\BatchCommandTransfer', $this->readAttribute($batch, 'transferStrategy')); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Batch/BatchClosureDivisorTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Batch/BatchClosureDivisorTest.php new file mode 100644 index 000000000..753db7dab --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Batch/BatchClosureDivisorTest.php @@ -0,0 +1,36 @@ +createBatches($queue); + $this->assertEquals(array(array('foo'), array('baz')), $batches); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Batch/BatchClosureTransferTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Batch/BatchClosureTransferTest.php new file mode 100644 index 000000000..6ba7ae052 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Batch/BatchClosureTransferTest.php @@ -0,0 +1,52 @@ +itemsTransferred = null; + $itemsTransferred =& $this->itemsTransferred; + + $this->transferStrategy = new BatchClosureTransfer(function (array $batch) use (&$itemsTransferred) { + $itemsTransferred = $batch; + return; + }); + } + + public function testTransfersBatch() + { + $batchedItems = array('foo', 'bar', 'baz'); + $this->transferStrategy->transfer($batchedItems); + + $this->assertEquals($batchedItems, $this->itemsTransferred); + } + + public function testTransferBailsOnEmptyBatch() + { + $batchedItems = array(); + $this->transferStrategy->transfer($batchedItems); + + $this->assertNull($this->itemsTransferred); + } + + /** + * @expectedException Guzzle\Common\Exception\InvalidArgumentException + */ + public function testEnsuresCallableIsCallable() + { + $foo = new BatchClosureTransfer('uh oh!'); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Batch/BatchCommandTransferTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Batch/BatchCommandTransferTest.php new file mode 100644 index 000000000..a04efabbb --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Batch/BatchCommandTransferTest.php @@ -0,0 +1,83 @@ + $command) { + if ($i % 2) { + $command->setClient($client1); + } else { + $command->setClient($client2); + } + $queue[] = $command; + } + + $batch = new BatchCommandTransfer(2); + $this->assertEquals(array( + array($commands[0], $commands[2]), + array($commands[4]), + array($commands[1], $commands[3]) + ), $batch->createBatches($queue)); + } + + /** + * @expectedException Guzzle\Common\Exception\InvalidArgumentException + */ + public function testEnsuresAllItemsAreCommands() + { + $queue = new \SplQueue(); + $queue[] = 'foo'; + $batch = new BatchCommandTransfer(2); + $batch->createBatches($queue); + } + + public function testTransfersBatches() + { + $client = $this->getMockBuilder('Guzzle\Service\Client') + ->setMethods(array('send')) + ->getMock(); + $client->expects($this->once()) + ->method('send'); + $command = new Mc(); + $command->setClient($client); + $batch = new BatchCommandTransfer(2); + $batch->transfer(array($command)); + } + + public function testDoesNotTransfersEmptyBatches() + { + $batch = new BatchCommandTransfer(2); + $batch->transfer(array()); + } + + /** + * @expectedException Guzzle\Service\Exception\InconsistentClientTransferException + */ + public function testEnsuresAllCommandsUseTheSameClient() + { + $batch = new BatchCommandTransfer(2); + $client1 = new Client('http://www.example.com'); + $client2 = new Client('http://www.example.com'); + $command1 = new Mc(); + $command1->setClient($client1); + $command2 = new Mc(); + $command2->setClient($client2); + $batch->transfer(array($command1, $command2)); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Batch/BatchRequestTransferTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Batch/BatchRequestTransferTest.php new file mode 100644 index 000000000..d929ef039 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Batch/BatchRequestTransferTest.php @@ -0,0 +1,80 @@ +setCurlMulti(new CurlMulti()); + + $client2 = new Client('http://www.example.com'); + $client2->setCurlMulti(new CurlMulti()); + + $request1 = $client1->get(); + $request2 = $client2->get(); + $request3 = $client1->get(); + $request4 = $client2->get(); + $request5 = $client1->get(); + + $queue = new \SplQueue(); + $queue[] = $request1; + $queue[] = $request2; + $queue[] = $request3; + $queue[] = $request4; + $queue[] = $request5; + + $batch = new BatchRequestTransfer(2); + $this->assertEquals(array( + array($request1, $request3), + array($request3), + array($request2, $request4) + ), $batch->createBatches($queue)); + } + + /** + * @expectedException \Guzzle\Common\Exception\InvalidArgumentException + */ + public function testEnsuresAllItemsAreRequests() + { + $queue = new \SplQueue(); + $queue[] = 'foo'; + $batch = new BatchRequestTransfer(2); + $batch->createBatches($queue); + } + + public function testTransfersBatches() + { + $client = new Client('http://localhost:123'); + $request = $client->get(); + // For some reason... PHP unit clones the request, which emits a request.clone event. This causes the + // 'sorted' property of the event dispatcher to contain an array in the cloned request that is not present in + // the original. + $request->dispatch('request.clone'); + + $multi = $this->getMock('Guzzle\Http\Curl\CurlMultiInterface'); + $client->setCurlMulti($multi); + $multi->expects($this->once()) + ->method('add') + ->with($request); + $multi->expects($this->once()) + ->method('send'); + + $batch = new BatchRequestTransfer(2); + $batch->transfer(array($request)); + } + + public function testDoesNotTransfersEmptyBatches() + { + $batch = new BatchRequestTransfer(2); + $batch->transfer(array()); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Batch/BatchSizeDivisorTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Batch/BatchSizeDivisorTest.php new file mode 100644 index 000000000..5542228fd --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Batch/BatchSizeDivisorTest.php @@ -0,0 +1,24 @@ +assertEquals(3, $d->getSize()); + $d->setSize(2); + $batches = $d->createBatches($queue); + $this->assertEquals(array(array('foo', 'baz'), array('bar')), $batches); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Batch/BatchTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Batch/BatchTest.php new file mode 100644 index 000000000..296f57aef --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Batch/BatchTest.php @@ -0,0 +1,91 @@ +getMock('Guzzle\Batch\BatchTransferInterface'); + } + + private function getMockDivisor() + { + return $this->getMock('Guzzle\Batch\BatchDivisorInterface'); + } + + public function testAddsItemsToQueue() + { + $batch = new Batch($this->getMockTransfer(), $this->getMockDivisor()); + $this->assertSame($batch, $batch->add('foo')); + $this->assertEquals(1, count($batch)); + } + + public function testFlushReturnsItems() + { + $transfer = $this->getMockTransfer(); + $transfer->expects($this->exactly(2)) + ->method('transfer'); + + $divisor = $this->getMockDivisor(); + $divisor->expects($this->once()) + ->method('createBatches') + ->will($this->returnValue(array(array('foo', 'baz'), array('bar')))); + + $batch = new Batch($transfer, $divisor); + + $batch->add('foo')->add('baz')->add('bar'); + $items = $batch->flush(); + + $this->assertEquals(array('foo', 'baz', 'bar'), $items); + } + + public function testThrowsExceptionContainingTheFailedBatch() + { + $called = 0; + $originalException = new \Exception('Foo!'); + + $transfer = $this->getMockTransfer(); + $transfer->expects($this->exactly(2)) + ->method('transfer') + ->will($this->returnCallback(function () use (&$called, $originalException) { + if (++$called == 2) { + throw $originalException; + } + })); + + $divisor = $this->getMockDivisor(); + $batch = new Batch($transfer, $divisor); + + // PHPunit clones objects before passing them to a callback. + // Horrible hack to get around this! + $queue = $this->readAttribute($batch, 'queue'); + + $divisor->expects($this->once()) + ->method('createBatches') + ->will($this->returnCallback(function ($batch) use ($queue) { + foreach ($queue as $item) { + $items[] = $item; + } + return array_chunk($items, 2); + })); + + $batch->add('foo')->add('baz')->add('bar')->add('bee')->add('boo'); + $this->assertFalse($batch->isEmpty()); + + try { + $items = $batch->flush(); + $this->fail('Expected exception'); + } catch (BatchTransferException $e) { + $this->assertEquals($originalException, $e->getPrevious()); + $this->assertEquals(array('bar', 'bee'), array_values($e->getBatch())); + $this->assertEquals(1, count($batch)); + } + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Batch/ExceptionBufferingBatchTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Batch/ExceptionBufferingBatchTest.php new file mode 100644 index 000000000..fd810b11f --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Batch/ExceptionBufferingBatchTest.php @@ -0,0 +1,45 @@ +getMockBuilder('Guzzle\Batch\BatchTransferInterface') + ->setMethods(array('transfer')) + ->getMock(); + + $d = new BatchSizeDivisor(1); + $batch = new Batch($t, $d); + + $called = 0; + $t->expects($this->exactly(3)) + ->method('transfer') + ->will($this->returnCallback(function ($batch) use (&$called) { + if (++$called === 2) { + throw new \Exception('Foo'); + } + })); + + $decorator = new ExceptionBufferingBatch($batch); + $decorator->add('foo')->add('baz')->add('bar'); + $result = $decorator->flush(); + + $e = $decorator->getExceptions(); + $this->assertEquals(1, count($e)); + $this->assertEquals(array('baz'), $e[0]->getBatch()); + + $decorator->clearExceptions(); + $this->assertEquals(0, count($decorator->getExceptions())); + + $this->assertEquals(array('foo', 'bar'), $result); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Batch/FlushingBatchTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Batch/FlushingBatchTest.php new file mode 100644 index 000000000..9b37a485f --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Batch/FlushingBatchTest.php @@ -0,0 +1,40 @@ +getMock('Guzzle\Batch\BatchTransferInterface', array('transfer')); + $d = $this->getMock('Guzzle\Batch\BatchDivisorInterface', array('createBatches')); + + $batch = new Batch($t, $d); + $queue = $this->readAttribute($batch, 'queue'); + + $d->expects($this->exactly(2)) + ->method('createBatches') + ->will($this->returnCallback(function () use ($queue) { + $items = array(); + foreach ($queue as $item) { + $items[] = $item; + } + return array($items); + })); + + $t->expects($this->exactly(2)) + ->method('transfer'); + + $flush = new FlushingBatch($batch, 3); + $this->assertEquals(3, $flush->getThreshold()); + $flush->setThreshold(2); + $flush->add('foo')->add('baz')->add('bar')->add('bee')->add('boo'); + $this->assertEquals(1, count($flush)); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Batch/HistoryBatchTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Batch/HistoryBatchTest.php new file mode 100644 index 000000000..60d6f951a --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Batch/HistoryBatchTest.php @@ -0,0 +1,26 @@ +getMock('Guzzle\Batch\BatchTransferInterface'), + $this->getMock('Guzzle\Batch\BatchDivisorInterface') + ); + + $history = new HistoryBatch($batch); + $history->add('foo')->add('baz'); + $this->assertEquals(array('foo', 'baz'), $history->getHistory()); + $history->clearHistory(); + $this->assertEquals(array(), $history->getHistory()); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Batch/NotifyingBatchTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Batch/NotifyingBatchTest.php new file mode 100644 index 000000000..69a89007a --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Batch/NotifyingBatchTest.php @@ -0,0 +1,45 @@ +getMock('Guzzle\Batch\Batch', array('flush'), array( + $this->getMock('Guzzle\Batch\BatchTransferInterface'), + $this->getMock('Guzzle\Batch\BatchDivisorInterface') + )); + + $batch->expects($this->once()) + ->method('flush') + ->will($this->returnValue(array('foo', 'baz'))); + + $data = array(); + $decorator = new NotifyingBatch($batch, function ($batch) use (&$data) { + $data[] = $batch; + }); + + $decorator->add('foo')->add('baz'); + $decorator->flush(); + $this->assertEquals(array(array('foo', 'baz')), $data); + } + + /** + * @expectedException Guzzle\Common\Exception\InvalidArgumentException + */ + public function testEnsuresCallableIsValid() + { + $batch = new Batch( + $this->getMock('Guzzle\Batch\BatchTransferInterface'), + $this->getMock('Guzzle\Batch\BatchDivisorInterface') + ); + $decorator = new NotifyingBatch($batch, 'foo'); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Cache/CacheAdapterFactoryTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Cache/CacheAdapterFactoryTest.php new file mode 100644 index 000000000..c4140a91d --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Cache/CacheAdapterFactoryTest.php @@ -0,0 +1,64 @@ +cache = new ArrayCache(); + $this->adapter = new DoctrineCacheAdapter($this->cache); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testEnsuresConfigIsObject() + { + CacheAdapterFactory::fromCache(array()); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testEnsuresKnownType() + { + CacheAdapterFactory::fromCache(new \stdClass()); + } + + public function cacheProvider() + { + return array( + array(new DoctrineCacheAdapter(new ArrayCache()), 'Guzzle\Cache\DoctrineCacheAdapter'), + array(new ArrayCache(), 'Guzzle\Cache\DoctrineCacheAdapter'), + array(StorageFactory::factory(array('adapter' => 'memory')), 'Guzzle\Cache\Zf2CacheAdapter'), + ); + } + + /** + * @dataProvider cacheProvider + */ + public function testCreatesNullCacheAdapterByDefault($cache, $type) + { + $adapter = CacheAdapterFactory::fromCache($cache); + $this->assertInstanceOf($type, $adapter); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Cache/CacheAdapterTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Cache/CacheAdapterTest.php new file mode 100644 index 000000000..3e30dddc5 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Cache/CacheAdapterTest.php @@ -0,0 +1,68 @@ +cache = new ArrayCache(); + $this->adapter = new DoctrineCacheAdapter($this->cache); + } + + /** + * Cleans up the environment after running a test. + */ + protected function tearDown() + { + $this->adapter = null; + $this->cache = null; + parent::tearDown(); + } + + public function testGetCacheObject() + { + $this->assertEquals($this->cache, $this->adapter->getCacheObject()); + } + + public function testSave() + { + $this->assertTrue($this->adapter->save('test', 'data', 1000)); + } + + public function testFetch() + { + $this->assertTrue($this->adapter->save('test', 'data', 1000)); + $this->assertEquals('data', $this->adapter->fetch('test')); + } + + public function testContains() + { + $this->assertTrue($this->adapter->save('test', 'data', 1000)); + $this->assertTrue($this->adapter->contains('test')); + } + + public function testDelete() + { + $this->assertTrue($this->adapter->save('test', 'data', 1000)); + $this->assertTrue($this->adapter->delete('test')); + $this->assertFalse($this->adapter->contains('test')); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Cache/ClosureCacheAdapterTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Cache/ClosureCacheAdapterTest.php new file mode 100644 index 000000000..12de65b5a --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Cache/ClosureCacheAdapterTest.php @@ -0,0 +1,94 @@ +callables = array( + 'contains' => function($id, $options = array()) use ($that) { + return array_key_exists($id, $that->data); + }, + 'delete' => function($id, $options = array()) use ($that) { + unset($that->data[$id]); + return true; + }, + 'fetch' => function($id, $options = array()) use ($that) { + return array_key_exists($id, $that->data) ? $that->data[$id] : null; + }, + 'save' => function($id, $data, $lifeTime, $options = array()) use ($that) { + $that->data[$id] = $data; + return true; + } + ); + + $this->adapter = new ClosureCacheAdapter($this->callables); + } + + /** + * Cleans up the environment after running a test. + */ + protected function tearDown() + { + $this->cache = null; + $this->callables = null; + parent::tearDown(); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testEnsuresCallablesArePresent() + { + $callables = $this->callables; + unset($callables['delete']); + $cache = new ClosureCacheAdapter($callables); + } + + public function testAllCallablesMustBePresent() + { + $cache = new ClosureCacheAdapter($this->callables); + } + + public function testCachesDataUsingCallables() + { + $this->assertTrue($this->adapter->save('test', 'data', 1000)); + $this->assertEquals('data', $this->adapter->fetch('test')); + } + + public function testChecksIfCacheContainsKeys() + { + $this->adapter->save('test', 'data', 1000); + $this->assertTrue($this->adapter->contains('test')); + $this->assertFalse($this->adapter->contains('foo')); + } + + public function testDeletesFromCacheByKey() + { + $this->adapter->save('test', 'data', 1000); + $this->assertTrue($this->adapter->contains('test')); + $this->adapter->delete('test'); + $this->assertFalse($this->adapter->contains('test')); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Cache/NullCacheAdapterTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Cache/NullCacheAdapterTest.php new file mode 100644 index 000000000..e05df3f78 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Cache/NullCacheAdapterTest.php @@ -0,0 +1,20 @@ +assertEquals(false, $c->contains('foo')); + $this->assertEquals(true, $c->delete('foo')); + $this->assertEquals(false, $c->fetch('foo')); + $this->assertEquals(true, $c->save('foo', 'bar')); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Cache/Zf2CacheAdapterTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Cache/Zf2CacheAdapterTest.php new file mode 100644 index 000000000..9077c12d3 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Cache/Zf2CacheAdapterTest.php @@ -0,0 +1,58 @@ +cache = StorageFactory::factory(array( + 'adapter' => 'memory' + )); + $this->adapter = new Zf2CacheAdapter($this->cache); + } + + /** + * Cleans up the environment after running a test. + */ + protected function tearDown() + { + $this->adapter = null; + $this->cache = null; + parent::tearDown(); + } + + public function testCachesDataUsingCallables() + { + $this->assertTrue($this->adapter->save('test', 'data', 1000)); + $this->assertEquals('data', $this->adapter->fetch('test')); + } + + public function testChecksIfCacheContainsKeys() + { + $this->adapter->save('test', 'data', 1000); + $this->assertTrue($this->adapter->contains('test')); + $this->assertFalse($this->adapter->contains('foo')); + } + + public function testDeletesFromCacheByKey() + { + $this->adapter->save('test', 'data', 1000); + $this->assertTrue($this->adapter->contains('test')); + $this->adapter->delete('test'); + $this->assertFalse($this->adapter->contains('test')); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Common/AbstractHasDispatcherTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Common/AbstractHasDispatcherTest.php new file mode 100644 index 000000000..19d12e6f6 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Common/AbstractHasDispatcherTest.php @@ -0,0 +1,63 @@ +assertEquals(array(), AbstractHasDispatcher::getAllEvents()); + } + + public function testAllowsDispatcherToBeInjected() + { + $d = new EventDispatcher(); + $mock = $this->getMockForAbstractClass('Guzzle\Common\AbstractHasDispatcher'); + $this->assertSame($mock, $mock->setEventDispatcher($d)); + $this->assertSame($d, $mock->getEventDispatcher()); + } + + public function testCreatesDefaultEventDispatcherIfNeeded() + { + $mock = $this->getMockForAbstractClass('Guzzle\Common\AbstractHasDispatcher'); + $this->assertInstanceOf('Symfony\Component\EventDispatcher\EventDispatcher', $mock->getEventDispatcher()); + } + + public function testHelperDispatchesEvents() + { + $data = array(); + $mock = $this->getMockForAbstractClass('Guzzle\Common\AbstractHasDispatcher'); + $mock->getEventDispatcher()->addListener('test', function(Event $e) use (&$data) { + $data = $e->getIterator()->getArrayCopy(); + }); + $mock->dispatch('test', array( + 'param' => 'abc' + )); + $this->assertEquals(array( + 'param' => 'abc', + ), $data); + } + + public function testHelperAttachesSubscribers() + { + $mock = $this->getMockForAbstractClass('Guzzle\Common\AbstractHasDispatcher'); + $subscriber = $this->getMockForAbstractClass('Symfony\Component\EventDispatcher\EventSubscriberInterface'); + + $dispatcher = $this->getMockBuilder('Symfony\Component\EventDispatcher\EventDispatcher') + ->setMethods(array('addSubscriber')) + ->getMock(); + + $dispatcher->expects($this->once()) + ->method('addSubscriber'); + + $mock->setEventDispatcher($dispatcher); + $mock->addSubscriber($subscriber); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Common/CollectionTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Common/CollectionTest.php new file mode 100644 index 000000000..0648a02b8 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Common/CollectionTest.php @@ -0,0 +1,529 @@ +coll = new Collection(); + } + + public function testConstructorCanBeCalledWithNoParams() + { + $this->coll = new Collection(); + $p = $this->coll->getAll(); + $this->assertEmpty($p, '-> Collection must be empty when no data is passed'); + } + + public function testConstructorCanBeCalledWithParams() + { + $testData = array( + 'test' => 'value', + 'test_2' => 'value2' + ); + $this->coll = new Collection($testData); + $this->assertEquals($this->coll->getAll(), $testData, '-> getAll() must return the data passed in the constructor'); + $this->assertEquals($this->coll->getAll(), $this->coll->toArray()); + } + + public function testImplementsIteratorAggregate() + { + $this->coll->set('key', 'value'); + $this->assertInstanceOf('ArrayIterator', $this->coll->getIterator()); + $this->assertEquals(1, count($this->coll)); + $total = 0; + foreach ($this->coll as $key => $value) { + $this->assertEquals('key', $key); + $this->assertEquals('value', $value); + $total++; + } + $this->assertEquals(1, $total); + } + + public function testCanAddValuesToExistingKeysByUsingArray() + { + $this->coll->add('test', 'value1'); + $this->assertEquals($this->coll->getAll(), array('test' => 'value1')); + $this->coll->add('test', 'value2'); + $this->assertEquals($this->coll->getAll(), array('test' => array('value1', 'value2'))); + $this->coll->add('test', 'value3'); + $this->assertEquals($this->coll->getAll(), array('test' => array('value1', 'value2', 'value3'))); + } + + public function testHandlesMergingInDisparateDataSources() + { + $params = array( + 'test' => 'value1', + 'test2' => 'value2', + 'test3' => array('value3', 'value4') + ); + $this->coll->merge($params); + $this->assertEquals($this->coll->getAll(), $params); + + // Pass the same object to itself + $this->assertEquals($this->coll->merge($this->coll), $this->coll); + } + + public function testCanClearAllDataOrSpecificKeys() + { + $this->coll->merge(array( + 'test' => 'value1', + 'test2' => 'value2' + )); + + // Clear a specific parameter by name + $this->coll->remove('test'); + + $this->assertEquals($this->coll->getAll(), array( + 'test2' => 'value2' + )); + + // Clear all parameters + $this->coll->clear(); + + $this->assertEquals($this->coll->getAll(), array()); + } + + public function testGetsValuesByKey() + { + $this->assertNull($this->coll->get('test')); + $this->coll->add('test', 'value'); + $this->assertEquals('value', $this->coll->get('test')); + $this->coll->set('test2', 'v2'); + $this->coll->set('test3', 'v3'); + $this->assertEquals(array( + 'test' => 'value', + 'test2' => 'v2' + ), $this->coll->getAll(array('test', 'test2'))); + } + + public function testProvidesKeys() + { + $this->assertEquals(array(), $this->coll->getKeys()); + $this->coll->merge(array( + 'test1' => 'value1', + 'test2' => 'value2' + )); + $this->assertEquals(array('test1', 'test2'), $this->coll->getKeys()); + // Returns the cached array previously returned + $this->assertEquals(array('test1', 'test2'), $this->coll->getKeys()); + $this->coll->remove('test1'); + $this->assertEquals(array('test2'), $this->coll->getKeys()); + $this->coll->add('test3', 'value3'); + $this->assertEquals(array('test2', 'test3'), $this->coll->getKeys()); + } + + public function testChecksIfHasKey() + { + $this->assertFalse($this->coll->hasKey('test')); + $this->coll->add('test', 'value'); + $this->assertEquals(true, $this->coll->hasKey('test')); + $this->coll->add('test2', 'value2'); + $this->assertEquals(true, $this->coll->hasKey('test')); + $this->assertEquals(true, $this->coll->hasKey('test2')); + $this->assertFalse($this->coll->hasKey('testing')); + $this->assertEquals(false, $this->coll->hasKey('AB-C', 'junk')); + } + + public function testChecksIfHasValue() + { + $this->assertFalse($this->coll->hasValue('value')); + $this->coll->add('test', 'value'); + $this->assertEquals('test', $this->coll->hasValue('value')); + $this->coll->add('test2', 'value2'); + $this->assertEquals('test', $this->coll->hasValue('value')); + $this->assertEquals('test2', $this->coll->hasValue('value2')); + $this->assertFalse($this->coll->hasValue('val')); + } + + public function testCanGetAllValuesByArray() + { + $this->coll->add('foo', 'bar'); + $this->coll->add('tEsT', 'value'); + $this->coll->add('tesTing', 'v2'); + $this->coll->add('key', 'v3'); + $this->assertNull($this->coll->get('test')); + $this->assertEquals(array( + 'foo' => 'bar', + 'tEsT' => 'value', + 'tesTing' => 'v2' + ), $this->coll->getAll(array( + 'foo', 'tesTing', 'tEsT' + ))); + } + + public function testImplementsCount() + { + $data = new Collection(); + $this->assertEquals(0, $data->count()); + $data->add('key', 'value'); + $this->assertEquals(1, count($data)); + $data->add('key', 'value2'); + $this->assertEquals(1, count($data)); + $data->add('key_2', 'value3'); + $this->assertEquals(2, count($data)); + } + + public function testAddParamsByMerging() + { + $params = array( + 'test' => 'value1', + 'test2' => 'value2', + 'test3' => array('value3', 'value4') + ); + + // Add some parameters + $this->coll->merge($params); + + // Add more parameters by merging them in + $this->coll->merge(array( + 'test' => 'another', + 'different_key' => 'new value' + )); + + $this->assertEquals(array( + 'test' => array('value1', 'another'), + 'test2' => 'value2', + 'test3' => array('value3', 'value4'), + 'different_key' => 'new value' + ), $this->coll->getAll()); + } + + public function testAllowsFunctionalFilter() + { + $this->coll->merge(array( + 'fruit' => 'apple', + 'number' => 'ten', + 'prepositions' => array('about', 'above', 'across', 'after'), + 'same_number' => 'ten' + )); + + $filtered = $this->coll->filter(function($key, $value) { + return $value == 'ten'; + }); + + $this->assertNotEquals($filtered, $this->coll); + + $this->assertEquals(array( + 'number' => 'ten', + 'same_number' => 'ten' + ), $filtered->getAll()); + } + + public function testAllowsFunctionalMapping() + { + $this->coll->merge(array( + 'number_1' => 1, + 'number_2' => 2, + 'number_3' => 3 + )); + + $mapped = $this->coll->map(function($key, $value) { + return $value * $value; + }); + + $this->assertNotEquals($mapped, $this->coll); + + $this->assertEquals(array( + 'number_1' => 1, + 'number_2' => 4, + 'number_3' => 9 + ), $mapped->getAll()); + } + + public function testImplementsArrayAccess() + { + $this->coll->merge(array( + 'k1' => 'v1', + 'k2' => 'v2' + )); + + $this->assertTrue($this->coll->offsetExists('k1')); + $this->assertFalse($this->coll->offsetExists('Krull')); + + $this->coll->offsetSet('k3', 'v3'); + $this->assertEquals('v3', $this->coll->offsetGet('k3')); + $this->assertEquals('v3', $this->coll->get('k3')); + + $this->coll->offsetUnset('k1'); + $this->assertFalse($this->coll->offsetExists('k1')); + } + + public function testUsesStaticWhenCreatingNew() + { + $qs = new QueryString(array( + 'a' => 'b', + 'c' => 'd' + )); + + $this->assertInstanceOf('Guzzle\\Http\\QueryString', $qs->map(function($a, $b) {})); + $this->assertInstanceOf('Guzzle\\Common\\Collection', $qs->map(function($a, $b) {}, array(), false)); + + $this->assertInstanceOf('Guzzle\\Http\\QueryString', $qs->filter(function($a, $b) {})); + $this->assertInstanceOf('Guzzle\\Common\\Collection', $qs->filter(function($a, $b) {}, false)); + } + + public function testCanReplaceAllData() + { + $this->assertSame($this->coll, $this->coll->replace(array( + 'a' => '123' + ))); + + $this->assertEquals(array( + 'a' => '123' + ), $this->coll->getAll()); + } + + public function dataProvider() + { + return array( + array('this_is_a_test', '{a}_is_a_{b}', array( + 'a' => 'this', + 'b' => 'test' + )), + array('this_is_a_test', '{abc}_is_a_{0}', array( + 'abc' => 'this', + 0 => 'test' + )), + array('this_is_a_test', '{abc}_is_a_{0}', array( + 'abc' => 'this', + 0 => 'test' + )), + array('this_is_a_test', 'this_is_a_test', array( + 'abc' => 'this' + )), + array('{abc}_is_{not_found}a_{0}', '{abc}_is_{not_found}a_{0}', array()) + ); + } + + /** + * @dataProvider dataProvider + */ + public function testInjectsConfigData($output, $input, $config) + { + $collection = new Collection($config); + $this->assertEquals($output, $collection->inject($input)); + } + + public function testCanSearchByKey() + { + $collection = new Collection(array( + 'foo' => 'bar', + 'BaZ' => 'pho' + )); + + $this->assertEquals('foo', $collection->keySearch('FOO')); + $this->assertEquals('BaZ', $collection->keySearch('baz')); + $this->assertEquals(false, $collection->keySearch('Bar')); + } + + public function testPreparesFromConfig() + { + $c = Collection::fromConfig(array( + 'a' => '123', + 'base_url' => 'http://www.test.com/' + ), array( + 'a' => 'xyz', + 'b' => 'lol' + ), array('a')); + + $this->assertInstanceOf('Guzzle\Common\Collection', $c); + $this->assertEquals(array( + 'a' => '123', + 'b' => 'lol', + 'base_url' => 'http://www.test.com/' + ), $c->getAll()); + + try { + $c = Collection::fromConfig(array(), array(), array('a')); + $this->fail('Exception not throw when missing config'); + } catch (InvalidArgumentException $e) { + } + } + + function falseyDataProvider() + { + return array( + array(false, false), + array(null, null), + array('', ''), + array(array(), array()), + array(0, 0), + ); + } + + /** + * @dataProvider falseyDataProvider + */ + public function testReturnsCorrectData($a, $b) + { + $c = new Collection(array('value' => $a)); + $this->assertSame($b, $c->get('value')); + } + + public function testRetrievesNestedKeysUsingPath() + { + $data = array( + 'foo' => 'bar', + 'baz' => array( + 'mesa' => array( + 'jar' => 'jar' + ) + ) + ); + $collection = new Collection($data); + $this->assertEquals('bar', $collection->getPath('foo')); + $this->assertEquals('jar', $collection->getPath('baz/mesa/jar')); + $this->assertNull($collection->getPath('wewewf')); + $this->assertNull($collection->getPath('baz/mesa/jar/jar')); + } + + public function testFalseyKeysStillDescend() + { + $collection = new Collection(array( + '0' => array( + 'a' => 'jar' + ), + 1 => 'other' + )); + $this->assertEquals('jar', $collection->getPath('0/a')); + $this->assertEquals('other', $collection->getPath('1')); + } + + public function getPathProvider() + { + $data = array( + 'foo' => 'bar', + 'baz' => array( + 'mesa' => array( + 'jar' => 'jar', + 'array' => array('a', 'b', 'c') + ), + 'bar' => array( + 'baz' => 'bam', + 'array' => array('d', 'e', 'f') + ) + ), + 'bam' => array( + array('foo' => 1), + array('foo' => 2), + array('array' => array('h', 'i')) + ) + ); + $c = new Collection($data); + + return array( + // Simple path selectors + array($c, 'foo', 'bar'), + array($c, 'baz', $data['baz']), + array($c, 'bam', $data['bam']), + array($c, 'baz/mesa', $data['baz']['mesa']), + array($c, 'baz/mesa/jar', 'jar'), + // Merge everything two levels under baz + array($c, 'baz/*', array( + 'jar' => 'jar', + 'array' => array_merge($data['baz']['mesa']['array'], $data['baz']['bar']['array']), + 'baz' => 'bam' + )), + // Does not barf on missing keys + array($c, 'fefwfw', null), + // Does not barf when a wildcard does not resolve correctly + array($c, '*/*/*/*/*/wefwfe', array()), + // Allows custom separator + array($c, '*|mesa', $data['baz']['mesa'], '|'), + // Merge all 'array' keys two levels under baz (the trailing * does not hurt the results) + array($c, 'baz/*/array/*', array_merge($data['baz']['mesa']['array'], $data['baz']['bar']['array'])), + // Merge all 'array' keys two levels under baz + array($c, 'baz/*/array', array_merge($data['baz']['mesa']['array'], $data['baz']['bar']['array'])), + array($c, 'baz/mesa/array', $data['baz']['mesa']['array']), + // Having a trailing * does not hurt the results + array($c, 'baz/mesa/array/*', $data['baz']['mesa']['array']), + // Merge of anything one level deep + array($c, '*', array_merge(array('bar'), $data['baz'], $data['bam'])), + // Funky merge of anything two levels deep + array($c, '*/*', array( + 'jar' => 'jar', + 'array' => array('a', 'b', 'c', 'd', 'e', 'f', 'h', 'i'), + 'baz' => 'bam', + 'foo' => array(1, 2) + )), + // Funky merge of all 'array' keys that are two levels deep + array($c, '*/*/array', array('a', 'b', 'c', 'd', 'e', 'f', 'h', 'i')) + ); + } + + /** + * @dataProvider getPathProvider + */ + public function testGetPath(Collection $c, $path, $expected, $separator = '/') + { + $this->assertEquals($expected, $c->getPath($path, $separator)); + } + + public function testOverridesSettings() + { + $c = new Collection(array('foo' => 1, 'baz' => 2, 'bar' => 3)); + $c->overwriteWith(array('foo' => 10, 'bar' => 300)); + $this->assertEquals(array('foo' => 10, 'baz' => 2, 'bar' => 300), $c->getAll()); + } + + public function testOverwriteWithCollection() + { + $c = new Collection(array('foo' => 1, 'baz' => 2, 'bar' => 3)); + $b = new Collection(array('foo' => 10, 'bar' => 300)); + $c->overwriteWith($b); + $this->assertEquals(array('foo' => 10, 'baz' => 2, 'bar' => 300), $c->getAll()); + } + + public function testOverwriteWithTraversable() + { + $c = new Collection(array('foo' => 1, 'baz' => 2, 'bar' => 3)); + $b = new Collection(array('foo' => 10, 'bar' => 300)); + $c->overwriteWith($b->getIterator()); + $this->assertEquals(array('foo' => 10, 'baz' => 2, 'bar' => 300), $c->getAll()); + } + + public function testCanSetNestedPathValueThatDoesNotExist() + { + $c = new Collection(array()); + $c->setPath('foo/bar/baz/123', 'hi'); + $this->assertEquals('hi', $c['foo']['bar']['baz']['123']); + } + + public function testCanSetNestedPathValueThatExists() + { + $c = new Collection(array('foo' => array('bar' => 'test'))); + $c->setPath('foo/bar', 'hi'); + $this->assertEquals('hi', $c['foo']['bar']); + } + + /** + * @expectedException \Guzzle\Common\Exception\RuntimeException + */ + public function testVerifiesNestedPathIsValidAtExactLevel() + { + $c = new Collection(array('foo' => 'bar')); + $c->setPath('foo/bar', 'hi'); + $this->assertEquals('hi', $c['foo']['bar']); + } + + /** + * @expectedException \Guzzle\Common\Exception\RuntimeException + */ + public function testVerifiesThatNestedPathIsValidAtAnyLevel() + { + $c = new Collection(array('foo' => 'bar')); + $c->setPath('foo/bar/baz', 'test'); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Common/EventTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Common/EventTest.php new file mode 100644 index 000000000..5484e1446 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Common/EventTest.php @@ -0,0 +1,62 @@ + '123', + 'other' => '456', + 'event' => 'test.notify' + )); + } + + public function testAllowsParameterInjection() + { + $event = new Event(array( + 'test' => '123' + )); + $this->assertEquals('123', $event['test']); + } + + public function testImplementsArrayAccess() + { + $event = $this->getEvent(); + $this->assertEquals('123', $event['test']); + $this->assertNull($event['foobar']); + + $this->assertTrue($event->offsetExists('test')); + $this->assertFalse($event->offsetExists('foobar')); + + unset($event['test']); + $this->assertFalse($event->offsetExists('test')); + + $event['test'] = 'new'; + $this->assertEquals('new', $event['test']); + } + + public function testImplementsIteratorAggregate() + { + $event = $this->getEvent(); + $this->assertInstanceOf('ArrayIterator', $event->getIterator()); + } + + public function testConvertsToArray() + { + $this->assertEquals(array( + 'test' => '123', + 'other' => '456', + 'event' => 'test.notify' + ), $this->getEvent()->toArray()); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Common/Exception/BatchTransferExceptionTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Common/Exception/BatchTransferExceptionTest.php new file mode 100644 index 000000000..c72a2a637 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Common/Exception/BatchTransferExceptionTest.php @@ -0,0 +1,21 @@ +getMock('Guzzle\Batch\BatchTransferInterface'); + $d = $this->getMock('Guzzle\Batch\BatchDivisorInterface'); + $transferException = new BatchTransferException(array('foo'), array(1, 2), $e, $t, $d); + $this->assertEquals(array('foo'), $transferException->getBatch()); + $this->assertSame($t, $transferException->getTransferStrategy()); + $this->assertSame($d, $transferException->getDivisorStrategy()); + $this->assertSame($e, $transferException->getPrevious()); + $this->assertEquals(array(1, 2), $transferException->getTransferredItems()); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Common/Exception/ExceptionCollectionTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Common/Exception/ExceptionCollectionTest.php new file mode 100644 index 000000000..2aecf2a06 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Common/Exception/ExceptionCollectionTest.php @@ -0,0 +1,66 @@ +getExceptions(); + $e->add($exceptions[0]); + $e->add($exceptions[1]); + $this->assertContains("(Exception) ./tests/Guzzle/Tests/Common/Exception/ExceptionCollectionTest.php line ", $e->getMessage()); + $this->assertContains(" Test\n\n #0 ./", $e->getMessage()); + $this->assertSame($exceptions[0], $e->getFirst()); + } + + public function testCanSetExceptions() + { + $ex = new \Exception('foo'); + $e = new ExceptionCollection(); + $e->setExceptions(array($ex)); + $this->assertSame($ex, $e->getFirst()); + } + + public function testActsAsArray() + { + $e = new ExceptionCollection(); + $exceptions = $this->getExceptions(); + $e->add($exceptions[0]); + $e->add($exceptions[1]); + $this->assertEquals(2, count($e)); + $this->assertEquals($exceptions, $e->getIterator()->getArrayCopy()); + } + + public function testCanAddSelf() + { + $e1 = new ExceptionCollection(); + $e1->add(new \Exception("Test")); + $e2 = new ExceptionCollection('Meta description!'); + $e2->add(new \Exception("Test 2")); + $e3 = new ExceptionCollection(); + $e3->add(new \Exception('Baz')); + $e2->add($e3); + $e1->add($e2); + $message = $e1->getMessage(); + $this->assertContains("(Exception) ./tests/Guzzle/Tests/Common/Exception/ExceptionCollectionTest.php line ", $message); + $this->assertContains("\n Test\n\n #0 ", $message); + $this->assertContains("\n\n(Guzzle\\Common\\Exception\\ExceptionCollection) ./tests/Guzzle/Tests/Common/Exception/ExceptionCollectionTest.php line ", $message); + $this->assertContains("\n\n Meta description!\n\n", $message); + $this->assertContains(" (Exception) ./tests/Guzzle/Tests/Common/Exception/ExceptionCollectionTest.php line ", $message); + $this->assertContains("\n Test 2\n\n #0 ", $message); + $this->assertContains(" (Exception) ./tests/Guzzle/Tests/Common/Exception/ExceptionCollectionTest.php line ", $message); + $this->assertContains(" Baz\n\n #0", $message); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Common/VersionTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Common/VersionTest.php new file mode 100644 index 000000000..c3a81d1e4 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Common/VersionTest.php @@ -0,0 +1,27 @@ +isRunning()) { + self::$server->flush(); + } else { + self::$server->start(); + } + } + + return self::$server; + } + + /** + * Set the service builder to use for tests + * + * @param ServiceBuilderInterface $builder Service builder + */ + public static function setServiceBuilder(ServiceBuilderInterface $builder) + { + self::$serviceBuilder = $builder; + } + + /** + * Get a service builder object that can be used throughout the service tests + * + * @return ServiceBuilder + */ + public static function getServiceBuilder() + { + if (!self::$serviceBuilder) { + throw new RuntimeException('No service builder has been set via setServiceBuilder()'); + } + + return self::$serviceBuilder; + } + + /** + * Check if an event dispatcher has a subscriber + * + * @param HasDispatcherInterface $dispatcher + * @param EventSubscriberInterface $subscriber + * + * @return bool + */ + protected function hasSubscriber(HasDispatcherInterface $dispatcher, EventSubscriberInterface $subscriber) + { + $class = get_class($subscriber); + $all = array_keys(call_user_func(array($class, 'getSubscribedEvents'))); + + foreach ($all as $i => $event) { + foreach ($dispatcher->getEventDispatcher()->getListeners($event) as $e) { + if ($e[0] === $subscriber) { + unset($all[$i]); + break; + } + } + } + + return count($all) == 0; + } + + /** + * Get a wildcard observer for an event dispatcher + * + * @param HasDispatcherInterface $hasEvent + * + * @return MockObserver + */ + public function getWildcardObserver(HasDispatcherInterface $hasDispatcher) + { + $class = get_class($hasDispatcher); + $o = new MockObserver(); + $events = call_user_func(array($class, 'getAllEvents')); + foreach ($events as $event) { + $hasDispatcher->getEventDispatcher()->addListener($event, array($o, 'update')); + } + + return $o; + } + + /** + * Set the mock response base path + * + * @param string $path Path to mock response folder + * + * @return GuzzleTestCase + */ + public static function setMockBasePath($path) + { + self::$mockBasePath = $path; + } + + /** + * Mark a request as being mocked + * + * @param RequestInterface $request + * + * @return self + */ + public function addMockedRequest(RequestInterface $request) + { + $this->requests[] = $request; + + return $this; + } + + /** + * Get all of the mocked requests + * + * @return array + */ + public function getMockedRequests() + { + return $this->requests; + } + + /** + * Get a mock response for a client by mock file name + * + * @param string $path Relative path to the mock response file + * + * @return Response + */ + public function getMockResponse($path) + { + return $path instanceof Response + ? $path + : MockPlugin::getMockFile(self::$mockBasePath . DIRECTORY_SEPARATOR . $path); + } + + /** + * Set a mock response from a mock file on the next client request. + * + * This method assumes that mock response files are located under the + * Command/Mock/ directory of the Service being tested + * (e.g. Unfuddle/Command/Mock/). A mock response is added to the next + * request sent by the client. + * + * @param Client $client Client object to modify + * @param string $paths Path to files within the Mock folder of the service + * + * @return MockPlugin returns the created mock plugin + */ + public function setMockResponse(Client $client, $paths) + { + $this->requests = array(); + $that = $this; + $mock = new MockPlugin(null, true); + $client->getEventDispatcher()->removeSubscriber($mock); + $mock->getEventDispatcher()->addListener('mock.request', function(Event $event) use ($that) { + $that->addMockedRequest($event['request']); + }); + + if ($paths instanceof Response) { + // A single response instance has been specified, create an array with that instance + // as the only element for the following loop to work as expected + $paths = array($paths); + } + + foreach ((array) $paths as $path) { + $mock->addResponse($this->getMockResponse($path)); + } + + $client->getEventDispatcher()->addSubscriber($mock); + + return $mock; + } + + /** + * Compare HTTP headers and use special markup to filter values + * A header prefixed with '!' means it must not exist + * A header prefixed with '_' means it must be ignored + * A header value of '*' means anything after the * will be ignored + * + * @param array $filteredHeaders Array of special headers + * @param array $actualHeaders Array of headers to check against + * + * @return array|bool Returns an array of the differences or FALSE if none + */ + public function compareHeaders($filteredHeaders, $actualHeaders) + { + $comparison = new HeaderComparison(); + + return $comparison->compare($filteredHeaders, $actualHeaders); + } + + /** + * Case insensitive assertContains + * + * @param string $needle Search string + * @param string $haystack Search this + * @param string $message Optional failure message + */ + public function assertContainsIns($needle, $haystack, $message = null) + { + $this->assertContains(strtolower($needle), strtolower($haystack), $message); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Http/AbstractEntityBodyDecoratorTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Http/AbstractEntityBodyDecoratorTest.php new file mode 100644 index 000000000..20feaa875 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Http/AbstractEntityBodyDecoratorTest.php @@ -0,0 +1,34 @@ +getMockForAbstractClass('Guzzle\Http\AbstractEntityBodyDecorator', array($e)); + + $this->assertSame($e->getStream(), $mock->getStream()); + $this->assertSame($e->getContentLength(), $mock->getContentLength()); + $this->assertSame($e->getSize(), $mock->getSize()); + $this->assertSame($e->getContentMd5(), $mock->getContentMd5()); + $this->assertSame($e->getContentType(), $mock->getContentType()); + $this->assertSame($e->__toString(), $mock->__toString()); + $this->assertSame($e->getUri(), $mock->getUri()); + $this->assertSame($e->getStreamType(), $mock->getStreamType()); + $this->assertSame($e->getWrapper(), $mock->getWrapper()); + $this->assertSame($e->getWrapperData(), $mock->getWrapperData()); + $this->assertSame($e->isReadable(), $mock->isReadable()); + $this->assertSame($e->isWritable(), $mock->isWritable()); + $this->assertSame($e->isConsumed(), $mock->isConsumed()); + $this->assertSame($e->isLocal(), $mock->isLocal()); + $this->assertSame($e->isSeekable(), $mock->isSeekable()); + $this->assertSame($e->getContentEncoding(), $mock->getContentEncoding()); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Http/CachingEntityBodyTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Http/CachingEntityBodyTest.php new file mode 100644 index 000000000..e6e6cdbf5 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Http/CachingEntityBodyTest.php @@ -0,0 +1,249 @@ +decorated = EntityBody::factory('testing'); + $this->body = new CachingEntityBody($this->decorated); + } + + public function testUsesRemoteSizeIfPossible() + { + $body = EntityBody::factory('test'); + $caching = new CachingEntityBody($body); + $this->assertEquals(4, $caching->getSize()); + $this->assertEquals(4, $caching->getContentLength()); + } + + /** + * @expectedException \Guzzle\Common\Exception\RuntimeException + * @expectedExceptionMessage does not support custom stream rewind + */ + public function testDoesNotAllowRewindFunction() + { + $this->body->setRewindFunction(true); + } + + /** + * @expectedException \Guzzle\Common\Exception\RuntimeException + * @expectedExceptionMessage Cannot seek to byte 10 + */ + public function testCannotSeekPastWhatHasBeenRead() + { + $this->body->seek(10); + } + + /** + * @expectedException \Guzzle\Common\Exception\RuntimeException + * @expectedExceptionMessage supports only SEEK_SET and SEEK_CUR + */ + public function testCannotUseSeekEnd() + { + $this->body->seek(2, SEEK_END); + } + + public function testChangingUnderlyingStreamUpdatesSizeAndStream() + { + $size = filesize(__FILE__); + $s = fopen(__FILE__, 'r'); + $this->body->setStream($s, $size); + $this->assertEquals($size, $this->body->getSize()); + $this->assertEquals($size, $this->decorated->getSize()); + $this->assertSame($s, $this->body->getStream()); + $this->assertSame($s, $this->decorated->getStream()); + } + + public function testRewindUsesSeek() + { + $a = EntityBody::factory('foo'); + $d = $this->getMockBuilder('Guzzle\Http\CachingEntityBody') + ->setMethods(array('seek')) + ->setConstructorArgs(array($a)) + ->getMock(); + $d->expects($this->once()) + ->method('seek') + ->with(0) + ->will($this->returnValue(true)); + $d->rewind(); + } + + public function testCanSeekToReadBytes() + { + $this->assertEquals('te', $this->body->read(2)); + $this->body->seek(0); + $this->assertEquals('test', $this->body->read(4)); + $this->assertEquals(4, $this->body->ftell()); + $this->body->seek(2); + $this->assertEquals(2, $this->body->ftell()); + $this->body->seek(2, SEEK_CUR); + $this->assertEquals(4, $this->body->ftell()); + $this->assertEquals('ing', $this->body->read(3)); + } + + public function testWritesToBufferStream() + { + $this->body->read(2); + $this->body->write('hi'); + $this->body->rewind(); + $this->assertEquals('tehiing', (string) $this->body); + } + + public function testReadLinesFromBothStreams() + { + $this->body->seek($this->body->ftell()); + $this->body->write("test\n123\nhello\n1234567890\n"); + $this->body->rewind(); + $this->assertEquals("test\n", $this->body->readLine(7)); + $this->assertEquals("123\n", $this->body->readLine(7)); + $this->assertEquals("hello\n", $this->body->readLine(7)); + $this->assertEquals("123456", $this->body->readLine(7)); + $this->assertEquals("7890\n", $this->body->readLine(7)); + // We overwrote the decorated stream, so no more data + $this->assertEquals('', $this->body->readLine(7)); + } + + public function testSkipsOverwrittenBytes() + { + $decorated = EntityBody::factory( + implode("\n", array_map(function ($n) { + return str_pad($n, 4, '0', STR_PAD_LEFT); + }, range(0, 25))) + ); + + $body = new CachingEntityBody($decorated); + + $this->assertEquals("0000\n", $body->readLine()); + $this->assertEquals("0001\n", $body->readLine()); + // Write over part of the body yet to be read, so skip some bytes + $this->assertEquals(5, $body->write("TEST\n")); + $this->assertEquals(5, $this->readAttribute($body, 'skipReadBytes')); + // Read, which skips bytes, then reads + $this->assertEquals("0003\n", $body->readLine()); + $this->assertEquals(0, $this->readAttribute($body, 'skipReadBytes')); + $this->assertEquals("0004\n", $body->readLine()); + $this->assertEquals("0005\n", $body->readLine()); + + // Overwrite part of the cached body (so don't skip any bytes) + $body->seek(5); + $this->assertEquals(5, $body->write("ABCD\n")); + $this->assertEquals(0, $this->readAttribute($body, 'skipReadBytes')); + $this->assertEquals("TEST\n", $body->readLine()); + $this->assertEquals("0003\n", $body->readLine()); + $this->assertEquals("0004\n", $body->readLine()); + $this->assertEquals("0005\n", $body->readLine()); + $this->assertEquals("0006\n", $body->readLine()); + $this->assertEquals(5, $body->write("1234\n")); + $this->assertEquals(5, $this->readAttribute($body, 'skipReadBytes')); + + // Seek to 0 and ensure the overwritten bit is replaced + $body->rewind(); + $this->assertEquals("0000\nABCD\nTEST\n0003\n0004\n0005\n0006\n1234\n0008\n0009\n", $body->read(50)); + + // Ensure that casting it to a string does not include the bit that was overwritten + $this->assertContains("0000\nABCD\nTEST\n0003\n0004\n0005\n0006\n1234\n0008\n0009\n", (string) $body); + } + + public function testWrapsContentType() + { + $a = $this->getMockBuilder('Guzzle\Http\EntityBody') + ->setMethods(array('getContentType')) + ->setConstructorArgs(array(fopen(__FILE__, 'r'))) + ->getMock(); + $a->expects($this->once()) + ->method('getContentType') + ->will($this->returnValue('foo')); + $d = new CachingEntityBody($a); + $this->assertEquals('foo', $d->getContentType()); + } + + public function testWrapsContentEncoding() + { + $a = $this->getMockBuilder('Guzzle\Http\EntityBody') + ->setMethods(array('getContentEncoding')) + ->setConstructorArgs(array(fopen(__FILE__, 'r'))) + ->getMock(); + $a->expects($this->once()) + ->method('getContentEncoding') + ->will($this->returnValue('foo')); + $d = new CachingEntityBody($a); + $this->assertEquals('foo', $d->getContentEncoding()); + } + + public function testWrapsMetadata() + { + $a = $this->getMockBuilder('Guzzle\Http\EntityBody') + ->setMethods(array('getMetadata', 'getWrapper', 'getWrapperData', 'getStreamType', 'getUri')) + ->setConstructorArgs(array(fopen(__FILE__, 'r'))) + ->getMock(); + + $a->expects($this->once()) + ->method('getMetadata') + ->will($this->returnValue(array())); + // Called twice for getWrapper and getWrapperData + $a->expects($this->exactly(1)) + ->method('getWrapper') + ->will($this->returnValue('wrapper')); + $a->expects($this->once()) + ->method('getWrapperData') + ->will($this->returnValue(array())); + $a->expects($this->once()) + ->method('getStreamType') + ->will($this->returnValue('baz')); + $a->expects($this->once()) + ->method('getUri') + ->will($this->returnValue('path/to/foo')); + + $d = new CachingEntityBody($a); + $this->assertEquals(array(), $d->getMetaData()); + $this->assertEquals('wrapper', $d->getWrapper()); + $this->assertEquals(array(), $d->getWrapperData()); + $this->assertEquals('baz', $d->getStreamType()); + $this->assertEquals('path/to/foo', $d->getUri()); + } + + public function testWrapsCustomData() + { + $a = $this->getMockBuilder('Guzzle\Http\EntityBody') + ->setMethods(array('getCustomData', 'setCustomData')) + ->setConstructorArgs(array(fopen(__FILE__, 'r'))) + ->getMock(); + + $a->expects($this->exactly(1)) + ->method('getCustomData') + ->with('foo') + ->will($this->returnValue('bar')); + + $a->expects($this->exactly(1)) + ->method('setCustomData') + ->with('foo', 'bar') + ->will($this->returnSelf()); + + $d = new CachingEntityBody($a); + $this->assertSame($d, $d->setCustomData('foo', 'bar')); + $this->assertEquals('bar', $d->getCustomData('foo')); + } + + public function testClosesBothStreams() + { + $s = fopen('php://temp', 'r'); + $a = EntityBody::factory($s); + $d = new CachingEntityBody($a); + $d->close(); + $this->assertFalse(is_resource($s)); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Http/ClientTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Http/ClientTest.php new file mode 100644 index 000000000..6814cacec --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Http/ClientTest.php @@ -0,0 +1,592 @@ +assertEquals('http://www.google.com/', $client->getBaseUrl()); + $this->assertSame($client, $client->setConfig(array( + 'test' => '123' + ))); + $this->assertEquals(array('test' => '123'), $client->getConfig()->getAll()); + $this->assertEquals('123', $client->getConfig('test')); + $this->assertSame($client, $client->setBaseUrl('http://www.test.com/{test}')); + $this->assertEquals('http://www.test.com/123', $client->getBaseUrl()); + $this->assertEquals('http://www.test.com/{test}', $client->getBaseUrl(false)); + + try { + $client->setConfig(false); + } catch (\InvalidArgumentException $e) { + } + } + + public function testDescribesEvents() + { + $this->assertEquals(array('client.create_request'), Client::getAllEvents()); + } + + public function testConstructorCanAcceptConfig() + { + $client = new Client('http://www.test.com/', array( + 'data' => '123' + )); + $this->assertEquals('123', $client->getConfig('data')); + } + + public function testCanUseCollectionAsConfig() + { + $client = new Client('http://www.google.com/'); + $client->setConfig(new Collection(array( + 'api' => 'v1', + 'key' => 'value', + 'base_url' => 'http://www.google.com/' + ))); + $this->assertEquals('v1', $client->getConfig('api')); + } + + public function testExpandsUriTemplatesUsingConfig() + { + $client = new Client('http://www.google.com/'); + $client->setConfig(array('api' => 'v1', 'key' => 'value', 'foo' => 'bar')); + $ref = new \ReflectionMethod($client, 'expandTemplate'); + $ref->setAccessible(true); + $this->assertEquals('Testing...api/v1/key/value', $ref->invoke($client, 'Testing...api/{api}/key/{key}')); + } + + public function testClientAttachersObserversToRequests() + { + $this->getServer()->flush(); + $this->getServer()->enqueue("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); + + $client = new Client($this->getServer()->getUrl()); + $logPlugin = $this->getLogPlugin(); + $client->getEventDispatcher()->addSubscriber($logPlugin); + + // Get a request from the client and ensure the the observer was + // attached to the new request + $request = $client->createRequest(); + $this->assertTrue($this->hasSubscriber($request, $logPlugin)); + } + + public function testClientReturnsValidBaseUrls() + { + $client = new Client('http://www.{foo}.{data}/', array( + 'data' => '123', + 'foo' => 'bar' + )); + $this->assertEquals('http://www.bar.123/', $client->getBaseUrl()); + $client->setBaseUrl('http://www.google.com/'); + $this->assertEquals('http://www.google.com/', $client->getBaseUrl()); + } + + public function testClientAddsCurlOptionsToRequests() + { + $client = new Client('http://www.test.com/', array( + 'api' => 'v1', + // Adds the option using the curl values + 'curl.options' => array( + 'CURLOPT_HTTPAUTH' => 'CURLAUTH_DIGEST', + 'abc' => 'foo', + 'blacklist' => 'abc', + 'debug' => true + ) + )); + + $request = $client->createRequest(); + $options = $request->getCurlOptions(); + $this->assertEquals(CURLAUTH_DIGEST, $options->get(CURLOPT_HTTPAUTH)); + $this->assertEquals('foo', $options->get('abc')); + $this->assertEquals('abc', $options->get('blacklist')); + } + + public function testClientAllowsFineGrainedSslControlButIsSecureByDefault() + { + $client = new Client('https://www.secure.com/'); + + // secure by default + $request = $client->createRequest(); + $options = $request->getCurlOptions(); + $this->assertTrue($options->get(CURLOPT_SSL_VERIFYPEER)); + + // set a capath if you prefer + $client = new Client('https://www.secure.com/'); + $client->setSslVerification(__DIR__); + $request = $client->createRequest(); + $options = $request->getCurlOptions(); + $this->assertSame(__DIR__, $options->get(CURLOPT_CAPATH)); + } + + public function testConfigSettingsControlSslConfiguration() + { + // Use the default ca certs on the system + $client = new Client('https://www.secure.com/', array('ssl.certificate_authority' => 'system')); + $this->assertNull($client->getConfig('curl.options')); + // Can set the cacert value as well + $client = new Client('https://www.secure.com/', array('ssl.certificate_authority' => false)); + $options = $client->getConfig('curl.options'); + $this->assertArrayNotHasKey(CURLOPT_CAINFO, $options); + $this->assertSame(false, $options[CURLOPT_SSL_VERIFYPEER]); + $this->assertSame(0, $options[CURLOPT_SSL_VERIFYHOST]); + } + + public function testClientAllowsUnsafeOperationIfRequested() + { + // be really unsafe if you insist + $client = new Client('https://www.secure.com/', array( + 'api' => 'v1' + )); + + $client->setSslVerification(false); + $request = $client->createRequest(); + $options = $request->getCurlOptions(); + $this->assertFalse($options->get(CURLOPT_SSL_VERIFYPEER)); + $this->assertNull($options->get(CURLOPT_CAINFO)); + } + + /** + * @expectedException \Guzzle\Common\Exception\RuntimeException + */ + public function testThrowsExceptionForInvalidCertificate() + { + $client = new Client('https://www.secure.com/'); + $client->setSslVerification('/path/to/missing/file'); + } + + public function testClientAllowsSettingSpecificSslCaInfo() + { + // set a file other than the provided cacert.pem + $client = new Client('https://www.secure.com/', array( + 'api' => 'v1' + )); + + $client->setSslVerification(__FILE__); + $request = $client->createRequest(); + $options = $request->getCurlOptions(); + $this->assertSame(__FILE__, $options->get(CURLOPT_CAINFO)); + } + + /** + * @expectedException Guzzle\Common\Exception\InvalidArgumentException + */ + public function testClientPreventsInadvertentInsecureVerifyHostSetting() + { + // set a file other than the provided cacert.pem + $client = new Client('https://www.secure.com/', array( + 'api' => 'v1' + )); + $client->setSslVerification(__FILE__, true, true); + } + + /** + * @expectedException Guzzle\Common\Exception\InvalidArgumentException + */ + public function testClientPreventsInvalidVerifyPeerSetting() + { + // set a file other than the provided cacert.pem + $client = new Client('https://www.secure.com/', array( + 'api' => 'v1' + )); + $client->setSslVerification(__FILE__, 'yes'); + } + + public function testClientAddsParamsToRequests() + { + Version::$emitWarnings = false; + $client = new Client('http://www.example.com', array( + 'api' => 'v1', + 'request.params' => array( + 'foo' => 'bar', + 'baz' => 'jar' + ) + )); + $request = $client->createRequest(); + $this->assertEquals('bar', $request->getParams()->get('foo')); + $this->assertEquals('jar', $request->getParams()->get('baz')); + Version::$emitWarnings = true; + } + + public function urlProvider() + { + $u = $this->getServer()->getUrl() . 'base/'; + $u2 = $this->getServer()->getUrl() . 'base?z=1'; + return array( + array($u, '', $u), + array($u, 'relative/path/to/resource', $u . 'relative/path/to/resource'), + array($u, 'relative/path/to/resource?a=b&c=d', $u . 'relative/path/to/resource?a=b&c=d'), + array($u, '/absolute/path/to/resource', $this->getServer()->getUrl() . 'absolute/path/to/resource'), + array($u, '/absolute/path/to/resource?a=b&c=d', $this->getServer()->getUrl() . 'absolute/path/to/resource?a=b&c=d'), + array($u2, '/absolute/path/to/resource?a=b&c=d', $this->getServer()->getUrl() . 'absolute/path/to/resource?a=b&c=d&z=1'), + array($u2, 'relative/path/to/resource', $this->getServer()->getUrl() . 'base/relative/path/to/resource?z=1'), + array($u2, 'relative/path/to/resource?another=query', $this->getServer()->getUrl() . 'base/relative/path/to/resource?another=query&z=1') + ); + } + + /** + * @dataProvider urlProvider + */ + public function testBuildsRelativeUrls($baseUrl, $url, $result) + { + $client = new Client($baseUrl); + $this->assertEquals($result, $client->get($url)->getUrl()); + } + + public function testAllowsConfigsToBeChangedAndInjectedInBaseUrl() + { + $client = new Client('http://{a}/{b}'); + $this->assertEquals('http:///', $client->getBaseUrl()); + $this->assertEquals('http://{a}/{b}', $client->getBaseUrl(false)); + $client->setConfig(array( + 'a' => 'test.com', + 'b' => 'index.html' + )); + $this->assertEquals('http://test.com/index.html', $client->getBaseUrl()); + } + + public function testCreatesRequestsWithDefaultValues() + { + $client = new Client($this->getServer()->getUrl() . 'base'); + + // Create a GET request + $request = $client->createRequest(); + $this->assertEquals('GET', $request->getMethod()); + $this->assertEquals($client->getBaseUrl(), $request->getUrl()); + + // Create a DELETE request + $request = $client->createRequest('DELETE'); + $this->assertEquals('DELETE', $request->getMethod()); + $this->assertEquals($client->getBaseUrl(), $request->getUrl()); + + // Create a HEAD request with custom headers + $request = $client->createRequest('HEAD', 'http://www.test.com/'); + $this->assertEquals('HEAD', $request->getMethod()); + $this->assertEquals('http://www.test.com/', $request->getUrl()); + + // Create a PUT request + $request = $client->createRequest('PUT'); + $this->assertEquals('PUT', $request->getMethod()); + + // Create a PUT request with injected config + $client->getConfig()->set('a', 1)->set('b', 2); + $request = $client->createRequest('PUT', '/path/{a}?q={b}'); + $this->assertEquals($request->getUrl(), $this->getServer()->getUrl() . 'path/1?q=2'); + } + + public function testClientHasHelperMethodsForCreatingRequests() + { + $url = $this->getServer()->getUrl(); + $client = new Client($url . 'base'); + $this->assertEquals('GET', $client->get()->getMethod()); + $this->assertEquals('PUT', $client->put()->getMethod()); + $this->assertEquals('POST', $client->post()->getMethod()); + $this->assertEquals('HEAD', $client->head()->getMethod()); + $this->assertEquals('DELETE', $client->delete()->getMethod()); + $this->assertEquals('OPTIONS', $client->options()->getMethod()); + $this->assertEquals('PATCH', $client->patch()->getMethod()); + $this->assertEquals($url . 'base/abc', $client->get('abc')->getUrl()); + $this->assertEquals($url . 'zxy', $client->put('/zxy')->getUrl()); + $this->assertEquals($url . 'zxy?a=b', $client->post('/zxy?a=b')->getUrl()); + $this->assertEquals($url . 'base?a=b', $client->head('?a=b')->getUrl()); + $this->assertEquals($url . 'base?a=b', $client->delete('/base?a=b')->getUrl()); + } + + public function testClientInjectsConfigsIntoUrls() + { + $client = new Client('http://www.test.com/api/v1', array( + 'test' => '123' + )); + $request = $client->get('relative/{test}'); + $this->assertEquals('http://www.test.com/api/v1/relative/123', $request->getUrl()); + } + + public function testAllowsEmptyBaseUrl() + { + $client = new Client(); + $request = $client->get('http://www.google.com/'); + $this->assertEquals('http://www.google.com/', $request->getUrl()); + $request->setResponse(new Response(200), true); + $request->send(); + } + + public function testAllowsCustomCurlMultiObjects() + { + $mock = $this->getMock('Guzzle\\Http\\Curl\\CurlMulti', array('add', 'send')); + $mock->expects($this->once()) + ->method('add') + ->will($this->returnSelf()); + $mock->expects($this->once()) + ->method('send') + ->will($this->returnSelf()); + + $client = new Client(); + $client->setCurlMulti($mock); + + $request = $client->get(); + $request->setResponse(new Response(200), true); + $client->send($request); + } + + public function testClientSendsMultipleRequests() + { + $client = new Client($this->getServer()->getUrl()); + $mock = new MockPlugin(); + + $responses = array( + new Response(200), + new Response(201), + new Response(202) + ); + + $mock->addResponse($responses[0]); + $mock->addResponse($responses[1]); + $mock->addResponse($responses[2]); + + $client->getEventDispatcher()->addSubscriber($mock); + + $requests = array( + $client->get(), + $client->head(), + $client->put('/', null, 'test') + ); + + $this->assertEquals(array( + $responses[0], + $responses[1], + $responses[2] + ), $client->send($requests)); + } + + public function testClientSendsSingleRequest() + { + $client = new Client($this->getServer()->getUrl()); + $mock = new MockPlugin(); + $response = new Response(200); + $mock->addResponse($response); + $client->getEventDispatcher()->addSubscriber($mock); + $this->assertEquals($response, $client->send($client->get())); + } + + /** + * @expectedException \Guzzle\Http\Exception\BadResponseException + */ + public function testClientThrowsExceptionForSingleRequest() + { + $client = new Client($this->getServer()->getUrl()); + $mock = new MockPlugin(); + $response = new Response(404); + $mock->addResponse($response); + $client->getEventDispatcher()->addSubscriber($mock); + $client->send($client->get()); + } + + /** + * @expectedException \Guzzle\Common\Exception\ExceptionCollection + */ + public function testClientThrowsExceptionForMultipleRequests() + { + $client = new Client($this->getServer()->getUrl()); + $mock = new MockPlugin(); + $mock->addResponse(new Response(200)); + $mock->addResponse(new Response(404)); + $client->getEventDispatcher()->addSubscriber($mock); + $client->send(array($client->get(), $client->head())); + } + + public function testQueryStringsAreNotDoubleEncoded() + { + $client = new Client('http://test.com', array( + 'path' => array('foo', 'bar'), + 'query' => 'hi there', + 'data' => array( + 'test' => 'a&b' + ) + )); + + $request = $client->get('{/path*}{?query,data*}'); + $this->assertEquals('http://test.com/foo/bar?query=hi%20there&test=a%26b', $request->getUrl()); + $this->assertEquals('hi there', $request->getQuery()->get('query')); + $this->assertEquals('a&b', $request->getQuery()->get('test')); + } + + public function testQueryStringsAreNotDoubleEncodedUsingAbsolutePaths() + { + $client = new Client('http://test.com', array( + 'path' => array('foo', 'bar'), + 'query' => 'hi there', + )); + $request = $client->get('http://test.com{?query}'); + $this->assertEquals('http://test.com?query=hi%20there', $request->getUrl()); + $this->assertEquals('hi there', $request->getQuery()->get('query')); + } + + public function testAllowsUriTemplateInjection() + { + $client = new Client('http://test.com'); + $ref = new \ReflectionMethod($client, 'getUriTemplate'); + $ref->setAccessible(true); + $a = $ref->invoke($client); + $this->assertSame($a, $ref->invoke($client)); + $client->setUriTemplate(new UriTemplate()); + $this->assertNotSame($a, $ref->invoke($client)); + } + + public function testAllowsCustomVariablesWhenExpandingTemplates() + { + $client = new Client('http://test.com', array('test' => 'hi')); + $ref = new \ReflectionMethod($client, 'expandTemplate'); + $ref->setAccessible(true); + $uri = $ref->invoke($client, 'http://{test}{?query*}', array('query' => array('han' => 'solo'))); + $this->assertEquals('http://hi?han=solo', $uri); + } + + public function testUriArrayAllowsCustomTemplateVariables() + { + $client = new Client(); + $vars = array( + 'var' => 'hi' + ); + $this->assertEquals('/hi', (string) $client->createRequest('GET', array('/{var}', $vars))->getUrl()); + $this->assertEquals('/hi', (string) $client->get(array('/{var}', $vars))->getUrl()); + $this->assertEquals('/hi', (string) $client->put(array('/{var}', $vars))->getUrl()); + $this->assertEquals('/hi', (string) $client->post(array('/{var}', $vars))->getUrl()); + $this->assertEquals('/hi', (string) $client->head(array('/{var}', $vars))->getUrl()); + $this->assertEquals('/hi', (string) $client->options(array('/{var}', $vars))->getUrl()); + } + + public function testAllowsDefaultHeaders() + { + Version::$emitWarnings = false; + $default = array('X-Test' => 'Hi!'); + $other = array('X-Other' => 'Foo'); + + $client = new Client(); + $client->setDefaultHeaders($default); + $this->assertEquals($default, $client->getDefaultHeaders()->getAll()); + $client->setDefaultHeaders(new Collection($default)); + $this->assertEquals($default, $client->getDefaultHeaders()->getAll()); + + $request = $client->createRequest('GET', null, $other); + $this->assertEquals('Hi!', $request->getHeader('X-Test')); + $this->assertEquals('Foo', $request->getHeader('X-Other')); + + $request = $client->createRequest('GET', null, new Collection($other)); + $this->assertEquals('Hi!', $request->getHeader('X-Test')); + $this->assertEquals('Foo', $request->getHeader('X-Other')); + + $request = $client->createRequest('GET'); + $this->assertEquals('Hi!', $request->getHeader('X-Test')); + Version::$emitWarnings = true; + } + + public function testDontReuseCurlMulti() + { + $client1 = new Client(); + $client2 = new Client(); + $this->assertNotSame($client1->getCurlMulti(), $client2->getCurlMulti()); + } + + public function testGetDefaultUserAgent() + { + $client = new Client(); + $agent = $this->readAttribute($client, 'userAgent'); + $version = curl_version(); + $testAgent = sprintf('Guzzle/%s curl/%s PHP/%s', Version::VERSION, $version['version'], PHP_VERSION); + $this->assertEquals($agent, $testAgent); + + $client->setUserAgent('foo'); + $this->assertEquals('foo', $this->readAttribute($client, 'userAgent')); + } + + public function testOverwritesUserAgent() + { + $client = new Client(); + $request = $client->createRequest('GET', 'http://www.foo.com', array('User-agent' => 'foo')); + $this->assertEquals('foo', (string) $request->getHeader('User-Agent')); + } + + public function testUsesDefaultUserAgent() + { + $client = new Client(); + $request = $client->createRequest('GET', 'http://www.foo.com'); + $this->assertContains('Guzzle/', (string) $request->getHeader('User-Agent')); + } + + public function testCanSetDefaultRequestOptions() + { + $client = new Client(); + $client->getConfig()->set('request.options', array( + 'query' => array('test' => '123', 'other' => 'abc'), + 'headers' => array('Foo' => 'Bar', 'Baz' => 'Bam') + )); + $request = $client->createRequest('GET', 'http://www.foo.com?test=hello', array('Foo' => 'Test')); + // Explicit options on a request should overrule default options + $this->assertEquals('Test', (string) $request->getHeader('Foo')); + $this->assertEquals('hello', $request->getQuery()->get('test')); + // Default options should still be set + $this->assertEquals('abc', $request->getQuery()->get('other')); + $this->assertEquals('Bam', (string) $request->getHeader('Baz')); + } + + public function testCanSetSetOptionsOnRequests() + { + $client = new Client(); + $request = $client->createRequest('GET', 'http://www.foo.com?test=hello', array('Foo' => 'Test'), null, array( + 'cookies' => array('michael' => 'test') + )); + $this->assertEquals('test', $request->getCookie('michael')); + } + + public function testHasDefaultOptionsHelperMethods() + { + $client = new Client(); + // With path + $client->setDefaultOption('headers/foo', 'bar'); + $this->assertEquals('bar', $client->getDefaultOption('headers/foo')); + // With simple key + $client->setDefaultOption('allow_redirects', false); + $this->assertFalse($client->getDefaultOption('allow_redirects')); + + $this->assertEquals(array( + 'headers' => array('foo' => 'bar'), + 'allow_redirects' => false + ), $client->getConfig('request.options')); + + $request = $client->get('/'); + $this->assertEquals('bar', $request->getHeader('foo')); + } + + public function testHeadCanUseOptions() + { + $client = new Client(); + $head = $client->head('http://www.foo.com', array(), array('query' => array('foo' => 'bar'))); + $this->assertEquals('bar', $head->getQuery()->get('foo')); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Http/Curl/CurlHandleTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Http/Curl/CurlHandleTest.php new file mode 100644 index 000000000..e9eab1d85 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Http/Curl/CurlHandleTest.php @@ -0,0 +1,945 @@ +getEventDispatcher()->addListener('request.sent', function (Event $e) use ($that) { + $that->requestHandle = $e['handle']; + }); + + return $request; + } + + public function setUp() + { + $this->requestHandle = null; + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testConstructorExpectsCurlResource() + { + $h = new CurlHandle(false, array()); + } + + public function testConstructorExpectsProperOptions() + { + $h = curl_init($this->getServer()->getUrl()); + try { + $ha = new CurlHandle($h, false); + $this->fail('Expected InvalidArgumentException'); + } catch (\InvalidArgumentException $e) { + } + + $ha = new CurlHandle($h, array( + CURLOPT_URL => $this->getServer()->getUrl() + )); + $this->assertEquals($this->getServer()->getUrl(), $ha->getOptions()->get(CURLOPT_URL)); + + $ha = new CurlHandle($h, new Collection(array( + CURLOPT_URL => $this->getServer()->getUrl() + ))); + $this->assertEquals($this->getServer()->getUrl(), $ha->getOptions()->get(CURLOPT_URL)); + } + + public function testConstructorInitializesObject() + { + $handle = curl_init($this->getServer()->getUrl()); + $h = new CurlHandle($handle, array( + CURLOPT_URL => $this->getServer()->getUrl() + )); + $this->assertSame($handle, $h->getHandle()); + $this->assertInstanceOf('Guzzle\\Http\\Url', $h->getUrl()); + $this->assertEquals($this->getServer()->getUrl(), (string) $h->getUrl()); + $this->assertEquals($this->getServer()->getUrl(), $h->getOptions()->get(CURLOPT_URL)); + } + + public function testStoresStdErr() + { + $request = RequestFactory::getInstance()->create('GET', 'http://test.com'); + $request->getCurlOptions()->set('debug', true); + $h = CurlHandle::factory($request); + $this->assertEquals($h->getStderr(true), $h->getOptions()->get(CURLOPT_STDERR)); + $this->assertInternalType('resource', $h->getStderr(true)); + $this->assertInternalType('string', $h->getStderr(false)); + $r = $h->getStderr(true); + fwrite($r, 'test'); + $this->assertEquals('test', $h->getStderr(false)); + } + + public function testStoresCurlErrorNumber() + { + $h = new CurlHandle(curl_init('http://test.com'), array(CURLOPT_URL => 'http://test.com')); + $this->assertEquals(CURLE_OK, $h->getErrorNo()); + $h->setErrorNo(CURLE_OPERATION_TIMEOUTED); + $this->assertEquals(CURLE_OPERATION_TIMEOUTED, $h->getErrorNo()); + } + + public function testAccountsForMissingStdErr() + { + $handle = curl_init('http://www.test.com/'); + $h = new CurlHandle($handle, array( + CURLOPT_URL => 'http://www.test.com/' + )); + $this->assertNull($h->getStderr(false)); + } + + public function testDeterminesIfResourceIsAvailable() + { + $handle = curl_init($this->getServer()->getUrl()); + $h = new CurlHandle($handle, array()); + $this->assertTrue($h->isAvailable()); + + // Mess it up by closing the handle + curl_close($handle); + $this->assertFalse($h->isAvailable()); + + // Mess it up by unsetting the handle + $handle = null; + $this->assertFalse($h->isAvailable()); + } + + public function testWrapsErrorsAndInfo() + { + if (!defined('CURLOPT_TIMEOUT_MS')) { + $this->markTestSkipped('Update curl'); + } + + $settings = array( + CURLOPT_PORT => 123, + CURLOPT_CONNECTTIMEOUT_MS => 1, + CURLOPT_TIMEOUT_MS => 1 + ); + + $handle = curl_init($this->getServer()->getUrl()); + curl_setopt_array($handle, $settings); + $h = new CurlHandle($handle, $settings); + @curl_exec($handle); + + $errors = array( + "couldn't connect to host", + 'timeout was reached', + 'connection time-out', + 'connect() timed out!' + ); + $this->assertTrue(in_array(strtolower($h->getError()), $errors), $h->getError() . ' was not the error'); + + $this->assertTrue($h->getErrorNo() > 0); + + $this->assertEquals($this->getServer()->getUrl(), $h->getInfo(CURLINFO_EFFECTIVE_URL)); + $this->assertInternalType('array', $h->getInfo()); + + curl_close($handle); + $this->assertEquals(null, $h->getInfo('url')); + } + + public function testGetInfoWithoutDebugMode() + { + $this->getServer()->enqueue("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); + $client = new Client($this->getServer()->getUrl()); + $request = $client->get($this->getServer()->getUrl()); + $response = $request->send(); + + $info = $response->getInfo(); + $this->assertFalse(empty($info)); + $this->assertEquals($this->getServer()->getUrl(), $info['url']); + } + + public function testWrapsCurlOptions() + { + $handle = curl_init($this->getServer()->getUrl()); + $h = new CurlHandle($handle, array( + CURLOPT_AUTOREFERER => true, + CURLOPT_BUFFERSIZE => 1024 + )); + + $this->assertEquals(true, $h->getOptions()->get(CURLOPT_AUTOREFERER)); + $this->assertEquals(1024, $h->getOptions()->get(CURLOPT_BUFFERSIZE)); + } + + /** + * Data provider for factory tests + * + * @return array + */ + public function dataProvider() + { + $testFile = __DIR__ . '/../../../../../phpunit.xml.dist'; + + $postBody = new QueryString(array('file' => '@' . $testFile)); + $qs = new QueryString(array( + 'x' => 'y', + 'z' => 'a' + )); + + $client = new Client(); + $userAgent = $client->getDefaultUserAgent(); + $auth = base64_encode('michael:123'); + $testFileSize = filesize($testFile); + + $tests = array( + // Send a regular GET + array('GET', 'http://www.google.com/', null, null, array( + CURLOPT_RETURNTRANSFER => 0, + CURLOPT_HEADER => 0, + CURLOPT_CONNECTTIMEOUT => 150, + CURLOPT_WRITEFUNCTION => 'callback', + CURLOPT_HEADERFUNCTION => 'callback', + CURLOPT_HTTPHEADER => array('Accept:', 'Host: www.google.com', 'User-Agent: ' . $userAgent), + )), + // Test that custom request methods can be used + array('TRACE', 'http://www.google.com/', null, null, array( + CURLOPT_CUSTOMREQUEST => 'TRACE' + )), + // Send a GET using a port + array('GET', 'http://127.0.0.1:8080', null, null, array( + CURLOPT_RETURNTRANSFER => 0, + CURLOPT_HEADER => 0, + CURLOPT_CONNECTTIMEOUT => 150, + CURLOPT_WRITEFUNCTION => 'callback', + CURLOPT_HEADERFUNCTION => 'callback', + CURLOPT_PORT => 8080, + CURLOPT_HTTPHEADER => array('Accept:', 'Host: 127.0.0.1:8080', 'User-Agent: ' . $userAgent), + )), + // Send a HEAD request + array('HEAD', 'http://www.google.com/', null, null, array( + CURLOPT_RETURNTRANSFER => 0, + CURLOPT_HEADER => 0, + CURLOPT_CONNECTTIMEOUT => 150, + CURLOPT_HEADERFUNCTION => 'callback', + CURLOPT_HTTPHEADER => array('Accept:', 'Host: www.google.com', 'User-Agent: ' . $userAgent), + CURLOPT_NOBODY => 1 + )), + // Send a GET using basic auth + array('GET', 'https://michael:123@localhost/index.html?q=2', null, null, array( + CURLOPT_RETURNTRANSFER => 0, + CURLOPT_HEADER => 0, + CURLOPT_CONNECTTIMEOUT => 150, + CURLOPT_WRITEFUNCTION => 'callback', + CURLOPT_HEADERFUNCTION => 'callback', + CURLOPT_HTTPHEADER => array( + 'Accept:', + 'Host: localhost', + 'Authorization: Basic ' . $auth, + 'User-Agent: ' . $userAgent + ), + CURLOPT_PORT => 443 + )), + // Send a GET request with custom headers + array('GET', 'http://localhost:8124/', array( + 'x-test-data' => 'Guzzle' + ), null, array( + CURLOPT_PORT => 8124, + CURLOPT_HTTPHEADER => array( + 'Accept:', + 'Host: localhost:8124', + 'x-test-data: Guzzle', + 'User-Agent: ' . $userAgent + ) + ), array( + 'Host' => '*', + 'User-Agent' => '*', + 'x-test-data' => 'Guzzle' + )), + // Send a POST using a query string + array('POST', 'http://localhost:8124/post.php', null, $qs, array( + CURLOPT_RETURNTRANSFER => 0, + CURLOPT_HEADER => 0, + CURLOPT_CONNECTTIMEOUT => 150, + CURLOPT_WRITEFUNCTION => 'callback', + CURLOPT_HEADERFUNCTION => 'callback', + CURLOPT_POSTFIELDS => 'x=y&z=a', + CURLOPT_HTTPHEADER => array ( + 'Expect:', + 'Accept:', + 'Host: localhost:8124', + 'Content-Type: application/x-www-form-urlencoded; charset=utf-8', + 'User-Agent: ' . $userAgent + ) + ), array( + 'Host' => '*', + 'User-Agent' => '*', + 'Content-Length' => '7', + '!Expect' => null, + 'Content-Type' => 'application/x-www-form-urlencoded; charset=utf-8', + '!Transfer-Encoding' => null + )), + // Send a PUT using raw data + array('PUT', 'http://localhost:8124/put.php', null, EntityBody::factory(fopen($testFile, 'r+')), array( + CURLOPT_RETURNTRANSFER => 0, + CURLOPT_HEADER => 0, + CURLOPT_CONNECTTIMEOUT => 150, + CURLOPT_WRITEFUNCTION => 'callback', + CURLOPT_HEADERFUNCTION => 'callback', + CURLOPT_READFUNCTION => 'callback', + CURLOPT_INFILESIZE => filesize($testFile), + CURLOPT_HTTPHEADER => array ( + 'Expect:', + 'Accept:', + 'Host: localhost:8124', + 'User-Agent: ' . $userAgent + ) + ), array( + 'Host' => '*', + 'User-Agent' => '*', + '!Expect' => null, + 'Content-Length' => $testFileSize, + '!Transfer-Encoding' => null + )), + // Send a POST request using an array of fields + array('POST', 'http://localhost:8124/post.php', null, array( + 'x' => 'y', + 'a' => 'b' + ), array( + CURLOPT_RETURNTRANSFER => 0, + CURLOPT_HEADER => 0, + CURLOPT_CONNECTTIMEOUT => 150, + CURLOPT_WRITEFUNCTION => 'callback', + CURLOPT_HEADERFUNCTION => 'callback', + CURLOPT_POST => 1, + CURLOPT_POSTFIELDS => 'x=y&a=b', + CURLOPT_HTTPHEADER => array ( + 'Expect:', + 'Accept:', + 'Host: localhost:8124', + 'Content-Type: application/x-www-form-urlencoded; charset=utf-8', + 'User-Agent: ' . $userAgent + ) + ), array( + 'Host' => '*', + 'User-Agent' => '*', + 'Content-Length' => '7', + '!Expect' => null, + 'Content-Type' => 'application/x-www-form-urlencoded; charset=utf-8', + '!Transfer-Encoding' => null + )), + // Send a POST request with raw POST data and a custom content-type + array('POST', 'http://localhost:8124/post.php', array( + 'Content-Type' => 'application/json' + ), '{"hi":"there"}', array( + CURLOPT_RETURNTRANSFER => 0, + CURLOPT_HEADER => 0, + CURLOPT_CONNECTTIMEOUT => 150, + CURLOPT_WRITEFUNCTION => 'callback', + CURLOPT_HEADERFUNCTION => 'callback', + CURLOPT_CUSTOMREQUEST => 'POST', + CURLOPT_UPLOAD => true, + CURLOPT_INFILESIZE => 14, + CURLOPT_HTTPHEADER => array ( + 'Expect:', + 'Accept:', + 'Host: localhost:8124', + 'Content-Type: application/json', + 'User-Agent: ' . $userAgent + ), + ), array( + 'Host' => '*', + 'User-Agent' => '*', + 'Content-Type' => 'application/json', + '!Expect' => null, + 'Content-Length' => '14', + '!Transfer-Encoding' => null + )), + // Send a POST request with raw POST data, a custom content-type, and use chunked encoding + array('POST', 'http://localhost:8124/post.php', array( + 'Content-Type' => 'application/json', + 'Transfer-Encoding' => 'chunked' + ), '{"hi":"there"}', array( + CURLOPT_RETURNTRANSFER => 0, + CURLOPT_HEADER => 0, + CURLOPT_CONNECTTIMEOUT => 150, + CURLOPT_WRITEFUNCTION => 'callback', + CURLOPT_HEADERFUNCTION => 'callback', + CURLOPT_CUSTOMREQUEST => 'POST', + CURLOPT_UPLOAD => true, + CURLOPT_HTTPHEADER => array ( + 'Expect:', + 'Accept:', + 'Host: localhost:8124', + 'Transfer-Encoding: chunked', + 'Content-Type: application/json', + 'User-Agent: ' . $userAgent + ), + ), array( + 'Host' => '*', + 'User-Agent' => '*', + 'Content-Type' => 'application/json', + '!Expect' => null, + 'Transfer-Encoding' => 'chunked', + '!Content-Length' => '' + )), + // Send a POST request with no body + array('POST', 'http://localhost:8124/post.php', null, '', array( + CURLOPT_CUSTOMREQUEST => 'POST', + CURLOPT_HTTPHEADER => array ( + 'Expect:', + 'Accept:', + 'Host: localhost:8124', + 'User-Agent: ' . $userAgent + ) + ), array( + 'Host' => '*', + 'User-Agent' => '*', + 'Content-Length' => '0', + '!Transfer-Encoding' => null + )), + // Send a POST request with empty post fields + array('POST', 'http://localhost:8124/post.php', null, array(), array( + CURLOPT_CUSTOMREQUEST => 'POST', + CURLOPT_HTTPHEADER => array ( + 'Expect:', + 'Accept:', + 'Host: localhost:8124', + 'User-Agent: ' . $userAgent + ) + ), array( + 'Host' => '*', + 'User-Agent' => '*', + 'Content-Length' => '0', + '!Transfer-Encoding' => null + )), + // Send a PATCH request + array('PATCH', 'http://localhost:8124/patch.php', null, 'body', array( + CURLOPT_INFILESIZE => 4, + CURLOPT_HTTPHEADER => array ( + 'Expect:', + 'Accept:', + 'Host: localhost:8124', + 'User-Agent: ' . $userAgent + ) + )), + // Send a DELETE request with a body + array('DELETE', 'http://localhost:8124/delete.php', null, 'body', array( + CURLOPT_CUSTOMREQUEST => 'DELETE', + CURLOPT_INFILESIZE => 4, + CURLOPT_HTTPHEADER => array ( + 'Expect:', + 'Accept:', + 'Host: localhost:8124', + 'User-Agent: ' . $userAgent + ) + ), array( + 'Host' => '*', + 'User-Agent' => '*', + 'Content-Length' => '4', + '!Expect' => null, + '!Transfer-Encoding' => null + )), + + /** + * Send a request with empty path and a fragment - the fragment must be + * stripped out before sending it to curl + * + * @issue 453 + * @link https://github.com/guzzle/guzzle/issues/453 + */ + array('GET', 'http://www.google.com#head', null, null, array( + CURLOPT_RETURNTRANSFER => 0, + CURLOPT_HEADER => 0, + CURLOPT_CONNECTTIMEOUT => 150, + CURLOPT_WRITEFUNCTION => 'callback', + CURLOPT_HEADERFUNCTION => 'callback', + CURLOPT_HTTPHEADER => array('Accept:', 'Host: www.google.com', 'User-Agent: ' . $userAgent), + )), + ); + + $postTest = array('POST', 'http://localhost:8124/post.php', null, $postBody, array( + CURLOPT_RETURNTRANSFER => 0, + CURLOPT_HEADER => 0, + CURLOPT_CONNECTTIMEOUT => 150, + CURLOPT_WRITEFUNCTION => 'callback', + CURLOPT_HEADERFUNCTION => 'callback', + CURLOPT_POST => 1, + CURLOPT_POSTFIELDS => array( + 'file' => '@' . $testFile . ';filename=phpunit.xml.dist;type=application/octet-stream' + ), + CURLOPT_HTTPHEADER => array ( + 'Accept:', + 'Host: localhost:8124', + 'Content-Type: multipart/form-data', + 'Expect: 100-Continue', + 'User-Agent: ' . $userAgent + ) + ), array( + 'Host' => '*', + 'User-Agent' => '*', + 'Content-Length' => '*', + 'Expect' => '100-Continue', + 'Content-Type' => 'multipart/form-data; boundary=*', + '!Transfer-Encoding' => null + )); + + if (version_compare(phpversion(), '5.5.0', '>=')) { + $postTest[4][CURLOPT_POSTFIELDS] = array( + 'file' => new \CurlFile($testFile, 'application/octet-stream', 'phpunit.xml.dist') + ); + } + + $tests[] = $postTest; + + return $tests; + } + + /** + * @dataProvider dataProvider + */ + public function testFactoryCreatesCurlBasedOnRequest($method, $url, $headers, $body, $options, $expectedHeaders = null) + { + $client = new Client(); + $request = $client->createRequest($method, $url, $headers, $body); + $request->getCurlOptions()->set('debug', true); + + $originalRequest = clone $request; + $curlTest = clone $request; + $handle = CurlHandle::factory($curlTest); + + $this->assertInstanceOf('Guzzle\\Http\\Curl\\CurlHandle', $handle); + $o = $handle->getOptions()->getAll(); + + // Headers are case-insensitive + if (isset($o[CURLOPT_HTTPHEADER])) { + $o[CURLOPT_HTTPHEADER] = array_map('strtolower', $o[CURLOPT_HTTPHEADER]); + } + if (isset($options[CURLOPT_HTTPHEADER])) { + $options[CURLOPT_HTTPHEADER] = array_map('strtolower', $options[CURLOPT_HTTPHEADER]); + } + + $check = 0; + foreach ($options as $key => $value) { + $check++; + $this->assertArrayHasKey($key, $o, '-> Check number ' . $check); + if ($key != CURLOPT_HTTPHEADER && $key != CURLOPT_POSTFIELDS && (is_array($o[$key])) || $o[$key] instanceof \Closure) { + $this->assertEquals('callback', $value, '-> Check number ' . $check); + } else { + $this->assertTrue($value == $o[$key], '-> Check number ' . $check . ' - ' . var_export($value, true) . ' != ' . var_export($o[$key], true)); + } + } + + // If we are testing the actual sent headers + if ($expectedHeaders) { + + // Send the request to the test server + $client = new Client($this->getServer()->getUrl()); + $request->setClient($client); + $this->getServer()->flush(); + $this->getServer()->enqueue("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); + $request->send(); + + // Get the request that was sent and create a request that we expected + $requests = $this->getServer()->getReceivedRequests(true); + $this->assertEquals($method, $requests[0]->getMethod()); + + $test = $this->compareHeaders($expectedHeaders, $requests[0]->getHeaders()); + $this->assertFalse($test, $test . "\nSent: \n" . $request . "\n\n" . $requests[0]); + + // Ensure only one Content-Length header is sent + if ($request->getHeader('Content-Length')) { + $this->assertEquals((string) $request->getHeader('Content-Length'), (string) $requests[0]->getHeader('Content-Length')); + } + } + } + + public function testFactoryUsesSpecifiedProtocol() + { + $request = RequestFactory::getInstance()->create('GET', 'http://localhost:8124/'); + $request->setProtocolVersion('1.1'); + $handle = CurlHandle::factory($request); + $options = $handle->getOptions(); + $this->assertEquals(CURL_HTTP_VERSION_1_1, $options[CURLOPT_HTTP_VERSION]); + } + + public function testUploadsPutData() + { + $this->getServer()->flush(); + $this->getServer()->enqueue("HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nhi"); + + $client = new Client($this->getServer()->getUrl()); + $request = $client->put('/'); + $request->getCurlOptions()->set('debug', true); + $request->setBody(EntityBody::factory('test'), 'text/plain', false); + $request->getCurlOptions()->set('progress', true); + + $o = $this->getWildcardObserver($request); + $request->send(); + + // Make sure that the events were dispatched + $this->assertTrue($o->has('curl.callback.progress')); + + // Ensure that the request was received exactly as intended + $r = $this->getServer()->getReceivedRequests(true); + $this->assertFalse($r[0]->hasHeader('Transfer-Encoding')); + $this->assertEquals(4, (string) $r[0]->getHeader('Content-Length')); + $sent = strtolower($r[0]); + $this->assertContains('put / http/1.1', $sent); + $this->assertContains('host: 127.0.0.1', $sent); + $this->assertContains('user-agent:', $sent); + $this->assertContains('content-type: text/plain', $sent); + } + + public function testUploadsPutDataUsingChunkedEncodingWhenLengthCannotBeDetermined() + { + $this->getServer()->flush(); + $this->getServer()->enqueue(array( + "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n", + "HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nhi" + )); + $client = new Client($this->getServer()->getUrl()); + $request = $client->put('/'); + $request->setBody(EntityBody::factory(fopen($this->getServer()->getUrl(), 'r')), 'text/plain'); + $request->send(); + + $r = $this->getServer()->getReceivedRequests(true); + $this->assertEquals('chunked', $r[1]->getHeader('Transfer-Encoding')); + $this->assertFalse($r[1]->hasHeader('Content-Length')); + } + + public function testUploadsPutDataUsingChunkedEncodingWhenForced() + { + $this->getServer()->flush(); + $this->getServer()->enqueue("HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nhi"); + + $client = new Client($this->getServer()->getUrl()); + $request = $client->put('/', array('Transfer-Encoding' => 'chunked'), 'hi!'); + $request->send(); + + $r = $this->getServer()->getReceivedRequests(true); + $this->assertEquals('chunked', $r[0]->getHeader('Transfer-Encoding')); + $this->assertFalse($r[0]->hasHeader('Content-Length')); + $this->assertEquals('hi!', $r[0]->getBody(true)); + } + + public function testSendsPostRequestsWithFields() + { + $this->getServer()->flush(); + $this->getServer()->enqueue("HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nhi"); + + $request = RequestFactory::getInstance()->create('POST', $this->getServer()->getUrl()); + $request->getCurlOptions()->set('debug', true); + $request->setClient(new Client()); + $request->addPostFields(array( + 'a' => 'b', + 'c' => 'ay! ~This is a test, isn\'t it?' + )); + $request->send(); + + // Make sure that the request was sent correctly + $r = $this->getServer()->getReceivedRequests(true); + $this->assertEquals('a=b&c=ay%21%20~This%20is%20a%20test%2C%20isn%27t%20it%3F', (string) $r[0]->getBody()); + $this->assertFalse($r[0]->hasHeader('Transfer-Encoding')); + $this->assertEquals(56, (string) $r[0]->getHeader('Content-Length')); + $sent = strtolower($r[0]); + $this->assertContains('post / http/1.1', $sent); + $this->assertContains('content-type: application/x-www-form-urlencoded; charset=utf-8', $sent); + } + + public function testSendsPostRequestsWithFiles() + { + $this->getServer()->flush(); + $this->getServer()->enqueue("HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nhi"); + + $request = RequestFactory::getInstance()->create('POST', $this->getServer()->getUrl()); + $request->getCurlOptions()->set('debug', true); + $request->setClient(new Client()); + $request->addPostFiles(array( + 'foo' => __FILE__, + )); + $request->addPostFields(array( + 'bar' => 'baz', + 'arr' => array('a' => 1, 'b' => 2), + )); + $this->updateForHandle($request); + $request->send(); + + // Ensure the CURLOPT_POSTFIELDS option was set properly + $options = $this->requestHandle->getOptions()->getAll(); + if (version_compare(phpversion(), '5.5.0', '<')) { + $this->assertContains('@' . __FILE__ . ';filename=CurlHandleTest.php;type=text/x-', $options[CURLOPT_POSTFIELDS]['foo']); + } else{ + $this->assertInstanceOf('CURLFile', $options[CURLOPT_POSTFIELDS]['foo']); + } + $this->assertEquals('baz', $options[CURLOPT_POSTFIELDS]['bar']); + $this->assertEquals('1', $options[CURLOPT_POSTFIELDS]['arr[a]']); + $this->assertEquals('2', $options[CURLOPT_POSTFIELDS]['arr[b]']); + // Ensure that a Content-Length header was sent by cURL + $this->assertTrue($request->hasHeader('Content-Length')); + } + + public function testCurlConfigurationOptionsAreSet() + { + $request = RequestFactory::getInstance()->create('PUT', $this->getServer()->getUrl()); + $request->setClient(new Client('http://www.example.com')); + $request->getCurlOptions()->set(CURLOPT_CONNECTTIMEOUT, 99); + $request->getCurlOptions()->set('curl.fake_opt', 99); + $request->getCurlOptions()->set(CURLOPT_PORT, 8181); + $handle = CurlHandle::factory($request); + $this->assertEquals(99, $handle->getOptions()->get(CURLOPT_CONNECTTIMEOUT)); + $this->assertEquals(8181, $handle->getOptions()->get(CURLOPT_PORT)); + $this->assertNull($handle->getOptions()->get('curl.fake_opt')); + $this->assertNull($handle->getOptions()->get('fake_opt')); + } + + public function testEnsuresRequestsHaveResponsesWhenUpdatingFromTransfer() + { + $request = RequestFactory::getInstance()->create('PUT', $this->getServer()->getUrl()); + $handle = CurlHandle::factory($request); + $handle->updateRequestFromTransfer($request); + } + + public function testCanSendBodyAsString() + { + $this->getServer()->flush(); + $this->getServer()->enqueue("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); + $client = new Client($this->getServer()->getUrl()); + $request = $client->put('/', null, 'foo'); + $request->getCurlOptions()->set('body_as_string', true); + $request->send(); + $requests = $this->getServer()->getReceivedRequests(false); + $this->assertContains('PUT /', $requests[0]); + $this->assertContains("\nfoo", $requests[0]); + $this->assertContains('content-length: 3', $requests[0]); + $this->assertNotContains('content-type', $requests[0]); + } + + public function testCanSendPostBodyAsString() + { + $this->getServer()->flush(); + $this->getServer()->enqueue("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); + $client = new Client($this->getServer()->getUrl()); + $request = $client->post('/', null, 'foo'); + $request->getCurlOptions()->set('body_as_string', true); + $request->send(); + $requests = $this->getServer()->getReceivedRequests(false); + $this->assertContains('POST /', $requests[0]); + $this->assertContains("\nfoo", $requests[0]); + $this->assertContains('content-length: 3', $requests[0]); + $this->assertNotContains('content-type', $requests[0]); + } + + public function testAllowsWireTransferInfoToBeEnabled() + { + $request = RequestFactory::getInstance()->create('PUT', $this->getServer()->getUrl()); + $request->getCurlOptions()->set('debug', true); + $handle = CurlHandle::factory($request); + $this->assertNotNull($handle->getOptions()->get(CURLOPT_STDERR)); + $this->assertNotNull($handle->getOptions()->get(CURLOPT_VERBOSE)); + } + + public function testAddsCustomCurlOptions() + { + $request = RequestFactory::getInstance()->create('PUT', $this->getServer()->getUrl()); + $request->getCurlOptions()->set(CURLOPT_TIMEOUT, 200); + $handle = CurlHandle::factory($request); + $this->assertEquals(200, $handle->getOptions()->get(CURLOPT_TIMEOUT)); + } + + public function testSendsPostUploadsWithContentDispositionHeaders() + { + $this->getServer()->flush(); + $this->getServer()->enqueue("HTTP/1.1 200 OK\r\n\r\nContent-Length: 0\r\n\r\n"); + + $fileToUpload = dirname(dirname(__DIR__)) . DIRECTORY_SEPARATOR . 'TestData' . DIRECTORY_SEPARATOR . 'test_service.json'; + + $client = new Client($this->getServer()->getUrl()); + $request = $client->post(); + $request->addPostFile('foo', $fileToUpload, 'application/json'); + $request->addPostFile('foo', __FILE__); + + $request->send(); + $requests = $this->getServer()->getReceivedRequests(true); + $body = (string) $requests[0]->getBody(); + + $this->assertContains('Content-Disposition: form-data; name="foo[0]"; filename="', $body); + $this->assertContains('Content-Type: application/json', $body); + $this->assertContains('Content-Type: text/x-', $body); + $this->assertContains('Content-Disposition: form-data; name="foo[1]"; filename="', $body); + } + + public function requestMethodProvider() + { + return array(array('POST'), array('PUT'), array('PATCH')); + } + + /** + * @dataProvider requestMethodProvider + */ + public function testSendsRequestsWithNoBodyUsingContentLengthZero($method) + { + $this->getServer()->flush(); + $this->getServer()->enqueue("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); + $client = new Client($this->getServer()->getUrl()); + $client->createRequest($method)->send(); + $requests = $this->getServer()->getReceivedRequests(true); + $this->assertFalse($requests[0]->hasHeader('Transfer-Encoding')); + $this->assertTrue($requests[0]->hasHeader('Content-Length')); + $this->assertEquals('0', (string) $requests[0]->getHeader('Content-Length')); + } + + /** + * @dataProvider provideCurlConfig + */ + public function testParseCurlConfigConvertsStringKeysToConstantKeys($options, $expected) + { + $actual = CurlHandle::parseCurlConfig($options); + $this->assertEquals($expected, $actual); + } + + /** + * Data provider for curl configurations + * + * @return array + */ + public function provideCurlConfig() + { + return array( + // Conversion of option name to constant value + array( + array( + 'CURLOPT_PORT' => 10, + 'CURLOPT_TIMEOUT' => 99 + ), + array( + CURLOPT_PORT => 10, + CURLOPT_TIMEOUT => 99 + ) + ), + // Keeps non constant options + array( + array('debug' => true), + array('debug' => true) + ), + // Conversion of constant names to constant values + array( + array('debug' => 'CURLPROXY_HTTP'), + array('debug' => CURLPROXY_HTTP) + ) + ); + } + + public function testSeeksToBeginningOfStreamWhenSending() + { + $this->getServer()->flush(); + $this->getServer()->enqueue(array( + "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n", + "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n" + )); + + $client = new Client($this->getServer()->getUrl()); + $request = $client->put('/', null, 'test'); + $request->send(); + $request->send(); + + $received = $this->getServer()->getReceivedRequests(true); + $this->assertEquals(2, count($received)); + $this->assertEquals('test', (string) $received[0]->getBody()); + $this->assertEquals('test', (string) $received[1]->getBody()); + } + + public function testAllowsCurloptEncodingToBeSet() + { + $this->getServer()->flush(); + $this->getServer()->enqueue("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); + + $client = new Client($this->getServer()->getUrl()); + $request = $client->get('/', null); + $request->getCurlOptions()->set(CURLOPT_ENCODING, ''); + $this->updateForHandle($request); + $request->send(); + $options = $this->requestHandle->getOptions()->getAll(); + $this->assertSame('', $options[CURLOPT_ENCODING]); + $received = $this->getServer()->getReceivedRequests(false); + $this->assertContainsIns('accept: */*', $received[0]); + $this->assertContainsIns('accept-encoding: ', $received[0]); + } + + public function testSendsExpectHeaderWhenSizeIsGreaterThanCutoff() + { + $this->getServer()->flush(); + $this->getServer()->enqueue("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); + $client = new Client($this->getServer()->getUrl()); + $request = $client->put('/', null, 'test'); + // Start sending the expect header to 2 bytes + $this->updateForHandle($request); + $request->setExpectHeaderCutoff(2)->send(); + $options = $this->requestHandle->getOptions()->getAll(); + $this->assertContains('Expect: 100-Continue', $options[CURLOPT_HTTPHEADER]); + $received = $this->getServer()->getReceivedRequests(false); + $this->assertContainsIns('expect: 100-continue', $received[0]); + } + + public function testSetsCurloptEncodingWhenAcceptEncodingHeaderIsSet() + { + $this->getServer()->flush(); + $this->getServer()->enqueue("HTTP/1.1 200 OK\r\nContent-Length: 4\r\n\r\ndata"); + $client = new Client($this->getServer()->getUrl()); + $request = $client->get('/', array( + 'Accept' => 'application/json', + 'Accept-Encoding' => 'gzip, deflate', + )); + $this->updateForHandle($request); + $request->send(); + $options = $this->requestHandle->getOptions()->getAll(); + $this->assertSame('gzip, deflate', $options[CURLOPT_ENCODING]); + $received = $this->getServer()->getReceivedRequests(false); + $this->assertContainsIns('accept: application/json', $received[0]); + $this->assertContainsIns('accept-encoding: gzip, deflate', $received[0]); + } + + public function testSendsPostFieldsForNonPostRequests() + { + $this->getServer()->flush(); + $this->getServer()->enqueue("HTTP/1.1 200 OK\r\n\r\nContent-Length: 0\r\n\r\n"); + + $client = new Client(); + $request = $client->put($this->getServer()->getUrl(), null, array( + 'foo' => 'baz', + 'baz' => 'bar' + )); + + $request->send(); + $requests = $this->getServer()->getReceivedRequests(true); + $this->assertEquals('PUT', $requests[0]->getMethod()); + $this->assertEquals( + 'application/x-www-form-urlencoded; charset=utf-8', + (string) $requests[0]->getHeader('Content-Type') + ); + $this->assertEquals(15, (string) $requests[0]->getHeader('Content-Length')); + $this->assertEquals('foo=baz&baz=bar', (string) $requests[0]->getBody()); + } + + public function testSendsPostFilesForNonPostRequests() + { + $this->getServer()->flush(); + $this->getServer()->enqueue("HTTP/1.1 200 OK\r\n\r\nContent-Length: 0\r\n\r\n"); + + $client = new Client(); + $request = $client->put($this->getServer()->getUrl(), null, array( + 'foo' => '@' . __FILE__ + )); + + $request->send(); + $requests = $this->getServer()->getReceivedRequests(true); + $this->assertEquals('PUT', $requests[0]->getMethod()); + $this->assertContains('multipart/form-data', (string) $requests[0]->getHeader('Content-Type')); + $this->assertContains('testSendsPostFilesForNonPostRequests', (string) $requests[0]->getBody()); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Http/Curl/CurlMultiProxyTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Http/Curl/CurlMultiProxyTest.php new file mode 100644 index 000000000..060cd59de --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Http/Curl/CurlMultiProxyTest.php @@ -0,0 +1,100 @@ +multi = new CurlMultiProxy(); + } + + public function tearDown() + { + unset($this->multi); + } + + public function testConstructorSetsMaxHandles() + { + $m = new CurlMultiProxy(2); + $this->assertEquals(2, $this->readAttribute($m, 'maxHandles')); + } + + public function testAddingRequestsAddsToQueue() + { + $r = new Request('GET', 'http://www.foo.com'); + $this->assertSame($this->multi, $this->multi->add($r)); + $this->assertEquals(1, count($this->multi)); + $this->assertEquals(array($r), $this->multi->all()); + + $this->assertTrue($this->multi->remove($r)); + $this->assertFalse($this->multi->remove($r)); + $this->assertEquals(0, count($this->multi)); + } + + public function testResetClearsState() + { + $r = new Request('GET', 'http://www.foo.com'); + $this->multi->add($r); + $this->multi->reset(); + $this->assertEquals(0, count($this->multi)); + } + + public function testSendWillSendQueuedRequestsFirst() + { + $this->getServer()->flush(); + $this->getServer()->enqueue(array( + "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n", + "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n" + )); + $client = new Client($this->getServer()->getUrl()); + $events = array(); + $client->getCurlMulti()->getEventDispatcher()->addListener( + CurlMultiProxy::ADD_REQUEST, + function ($e) use (&$events) { + $events[] = $e; + } + ); + $request = $client->get(); + $request->getEventDispatcher()->addListener('request.complete', function () use ($client) { + $client->get('/foo')->send(); + }); + $request->send(); + $received = $this->getServer()->getReceivedRequests(true); + $this->assertEquals(2, count($received)); + $this->assertEquals($this->getServer()->getUrl(), $received[0]->getUrl()); + $this->assertEquals($this->getServer()->getUrl() . 'foo', $received[1]->getUrl()); + $this->assertEquals(2, count($events)); + } + + public function testTrimsDownMaxHandleCount() + { + $this->getServer()->flush(); + $this->getServer()->enqueue(array( + "HTTP/1.1 307 OK\r\nLocation: /foo\r\nContent-Length: 0\r\n\r\n", + "HTTP/1.1 307 OK\r\nLocation: /foo\r\nContent-Length: 0\r\n\r\n", + "HTTP/1.1 307 OK\r\nLocation: /foo\r\nContent-Length: 0\r\n\r\n", + "HTTP/1.1 307 OK\r\nLocation: /foo\r\nContent-Length: 0\r\n\r\n", + "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n" + )); + $client = new Client($this->getServer()->getUrl()); + $client->setCurlMulti(new CurlMultiProxy(2)); + $request = $client->get(); + $request->send(); + $this->assertEquals(200, $request->getResponse()->getStatusCode()); + $handles = $this->readAttribute($client->getCurlMulti(), 'handles'); + $this->assertEquals(2, count($handles)); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Http/Curl/CurlMultiTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Http/Curl/CurlMultiTest.php new file mode 100644 index 000000000..a21f73b5d --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Http/Curl/CurlMultiTest.php @@ -0,0 +1,455 @@ +multi = new MockMulti(); + } + + public function tearDown() + { + unset($this->multi); + } + + public function testConstructorCreateMultiHandle() + { + $this->assertInternalType('resource', $this->multi->getHandle()); + $this->assertEquals('curl_multi', get_resource_type($this->multi->getHandle())); + } + + public function testDestructorClosesMultiHandle() + { + $handle = $this->multi->getHandle(); + $this->multi->__destruct(); + $this->assertFalse(is_resource($handle)); + } + + public function testRequestsCanBeAddedAndCounted() + { + $multi = new CurlMulti(); + $request1 = new Request('GET', 'http://www.google.com/'); + $multi->add($request1); + $this->assertEquals(array($request1), $multi->all()); + $request2 = new Request('POST', 'http://www.google.com/'); + $multi->add($request2); + $this->assertEquals(array($request1, $request2), $multi->all()); + $this->assertEquals(2, count($multi)); + } + + public function testRequestsCanBeRemoved() + { + $request1 = new Request('GET', 'http://www.google.com/'); + $this->multi->add($request1); + $request2 = new Request('PUT', 'http://www.google.com/'); + $this->multi->add($request2); + $this->assertEquals(array($request1, $request2), $this->multi->all()); + $this->assertTrue($this->multi->remove($request1)); + $this->assertFalse($this->multi->remove($request1)); + $this->assertEquals(array($request2), $this->multi->all()); + } + + public function testsResetRemovesRequestsAndResetsState() + { + $this->multi->add(new Request('GET', 'http://www.google.com/')); + $this->multi->reset(); + $this->assertEquals(array(), $this->multi->all()); + } + + public function testSendsRequestsThroughCurl() + { + $this->getServer()->enqueue(array( + "HTTP/1.1 204 No content\r\n" . + "Content-Length: 0\r\n" . + "Server: Jetty(6.1.3)\r\n\r\n", + "HTTP/1.1 200 OK\r\n" . + "Content-Type: text/html; charset=utf-8\r\n" . + "Content-Length: 4\r\n" . + "Server: Jetty(6.1.3)\r\n\r\n" . + "data" + )); + + $request1 = new Request('GET', $this->getServer()->getUrl()); + $request2 = new Request('GET', $this->getServer()->getUrl()); + $this->multi->add($request1); + $this->multi->add($request2); + $this->multi->send(); + + $response1 = $request1->getResponse(); + $response2 = $request2->getResponse(); + $this->assertInstanceOf('Guzzle\\Http\\Message\\Response', $response1); + $this->assertInstanceOf('Guzzle\\Http\\Message\\Response', $response2); + + $this->assertTrue($response1->getBody(true) == 'data' || $response2->getBody(true) == 'data'); + $this->assertTrue($response1->getBody(true) == '' || $response2->getBody(true) == ''); + $this->assertTrue($response1->getStatusCode() == '204' || $response2->getStatusCode() == '204'); + $this->assertNotEquals((string) $response1, (string) $response2); + } + + public function testSendsThroughCurlAndAggregatesRequestExceptions() + { + $this->getServer()->enqueue(array( + "HTTP/1.1 200 OK\r\n" . + "Content-Type: text/html; charset=utf-8\r\n" . + "Content-Length: 4\r\n" . + "Server: Jetty(6.1.3)\r\n" . + "\r\n" . + "data", + "HTTP/1.1 204 No content\r\n" . + "Content-Length: 0\r\n" . + "Server: Jetty(6.1.3)\r\n" . + "\r\n", + "HTTP/1.1 404 Not Found\r\n" . + "Content-Length: 0\r\n" . + "\r\n" + )); + + $request1 = new Request('GET', $this->getServer()->getUrl()); + $request2 = new Request('HEAD', $this->getServer()->getUrl()); + $request3 = new Request('GET', $this->getServer()->getUrl()); + $this->multi->add($request1); + $this->multi->add($request2); + $this->multi->add($request3); + + try { + $this->multi->send(); + $this->fail('MultiTransferException not thrown when aggregating request exceptions'); + } catch (MultiTransferException $e) { + + $this->assertTrue($e->containsRequest($request1)); + $this->assertTrue($e->containsRequest($request2)); + $this->assertTrue($e->containsRequest($request3)); + $this->assertInstanceOf('ArrayIterator', $e->getIterator()); + $this->assertEquals(1, count($e)); + $exceptions = $e->getIterator(); + + $response1 = $request1->getResponse(); + $response2 = $request2->getResponse(); + $response3 = $request3->getResponse(); + + $this->assertNotEquals((string) $response1, (string) $response2); + $this->assertNotEquals((string) $response3, (string) $response1); + $this->assertInstanceOf('Guzzle\\Http\\Message\\Response', $response1); + $this->assertInstanceOf('Guzzle\\Http\\Message\\Response', $response2); + $this->assertInstanceOf('Guzzle\\Http\\Message\\Response', $response3); + + $failed = $exceptions[0]->getResponse(); + $this->assertEquals(404, $failed->getStatusCode()); + $this->assertEquals(1, count($e)); + + // Test the IteratorAggregate functionality + foreach ($e as $except) { + $this->assertEquals($failed, $except->getResponse()); + } + + $this->assertEquals(1, count($e->getFailedRequests())); + $this->assertEquals(2, count($e->getSuccessfulRequests())); + $this->assertEquals(3, count($e->getAllRequests())); + } + } + + public function testCurlErrorsAreCaught() + { + $this->getServer()->enqueue("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); + try { + $request = RequestFactory::getInstance()->create('GET', 'http://127.0.0.1:9876/'); + $request->setClient(new Client()); + $request->getCurlOptions()->set(CURLOPT_FRESH_CONNECT, true); + $request->getCurlOptions()->set(CURLOPT_FORBID_REUSE, true); + $request->getCurlOptions()->set(CURLOPT_CONNECTTIMEOUT_MS, 5); + $request->send(); + $this->fail('CurlException not thrown'); + } catch (CurlException $e) { + $m = $e->getMessage(); + $this->assertContains('[curl] ', $m); + $this->assertContains('[url] http://127.0.0.1:9876/', $m); + $this->assertInternalType('array', $e->getCurlInfo()); + } + } + + public function testRemovesQueuedRequests() + { + $request = RequestFactory::getInstance()->create('GET', 'http://127.0.0.1:9876/'); + $r = new Response(200); + $request->setClient(new Client()); + $request->setResponse($r, true); + $this->multi->add($request); + $this->multi->send(); + $this->assertSame($r, $request->getResponse()); + } + + public function testRemovesQueuedRequestsAddedInTransit() + { + $this->getServer()->flush(); + $this->getServer()->enqueue(array("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n")); + $client = new Client($this->getServer()->getUrl()); + $r = $client->get(); + $r->getEventDispatcher()->addListener('request.receive.status_line', function (Event $event) use ($client) { + // Create a request using a queued response + $request = $client->get()->setResponse(new Response(200), true); + $request->send(); + }); + $r->send(); + $this->assertEquals(1, count($this->getServer()->getReceivedRequests(false))); + } + + public function testCatchesExceptionsBeforeSendingSingleRequest() + { + $client = new Client($this->getServer()->getUrl()); + $multi = new CurlMulti(); + $client->setCurlMulti($multi); + $request = $client->get(); + $request->getEventDispatcher()->addListener('request.before_send', function() { + throw new \RuntimeException('Testing!'); + }); + try { + $request->send(); + $this->fail('Did not throw'); + } catch (\RuntimeException $e) { + // Ensure it was removed + $this->assertEquals(0, count($multi)); + } + } + + /** + * @expectedException \Guzzle\Common\Exception\ExceptionCollection + * @expectedExceptionMessage Thrown before sending! + */ + public function testCatchesExceptionsBeforeSendingMultipleRequests() + { + $client = new Client($this->getServer()->getUrl()); + $request = $client->get(); + $request->getEventDispatcher()->addListener('request.before_send', function() { + throw new \RuntimeException('Thrown before sending!'); + }); + $client->send(array($request)); + } + + public function testCatchesExceptionsWhenRemovingQueuedRequests() + { + $this->getServer()->enqueue("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); + $client = new Client($this->getServer()->getUrl()); + $r = $client->get(); + $r->getEventDispatcher()->addListener('request.sent', function() use ($client) { + // Create a request using a queued response + $client->get()->setResponse(new Response(404), true)->send(); + }); + try { + $r->send(); + $this->fail('Did not throw'); + } catch (BadResponseException $e) { + $this->assertCount(0, $client->getCurlMulti()); + } + } + + public function testCatchesExceptionsWhenRemovingQueuedRequestsBeforeSending() + { + $this->getServer()->enqueue("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); + $client = new Client($this->getServer()->getUrl()); + $r = $client->get(); + $r->getEventDispatcher()->addListener('request.before_send', function() use ($client) { + // Create a request using a queued response + $client->get()->setResponse(new Response(404), true)->send(); + }); + try { + $r->send(); + $this->fail('Did not throw'); + } catch (BadResponseException $e) { + $this->assertCount(0, $client->getCurlMulti()); + } + } + + /** + * @expectedException \RuntimeException + * @expectedExceptionMessage test + */ + public function testDoesNotCatchRandomExceptionsThrownDuringPerform() + { + $client = new Client($this->getServer()->getUrl()); + $multi = $this->getMock('Guzzle\\Http\\Curl\\CurlMulti', array('perform')); + $multi->expects($this->once()) + ->method('perform') + ->will($this->throwException(new \RuntimeException('test'))); + $multi->add($client->get()); + $multi->send(); + } + + public function testDoesNotSendRequestsDecliningToBeSent() + { + if (!defined('CURLOPT_TIMEOUT_MS')) { + $this->markTestSkipped('Update curl'); + } + + // Create a client that is bound to fail connecting + $client = new Client('http://localhost:123', array( + 'curl.CURLOPT_PORT' => 123, + 'curl.CURLOPT_CONNECTTIMEOUT_MS' => 1, + )); + + $request = $client->get(); + $multi = new CurlMulti(); + $multi->add($request); + + // Listen for request exceptions, and when they occur, first change the + // state of the request back to transferring, and then just allow it to + // exception out + $request->getEventDispatcher()->addListener('request.exception', function(Event $event) use ($multi) { + $retries = $event['request']->getParams()->get('retries'); + // Allow the first failure to retry + if ($retries == 0) { + $event['request']->setState('transfer'); + $event['request']->getParams()->set('retries', 1); + // Remove the request to try again + $multi->remove($event['request']); + $multi->add($event['request']); + } + }); + + try { + $multi->send(); + $this->fail('Did not throw an exception at all!?!'); + } catch (\Exception $e) { + $this->assertEquals(1, $request->getParams()->get('retries')); + } + } + + public function testDoesNotThrowExceptionsWhenRequestsRecoverWithRetry() + { + $this->getServer()->flush(); + $client = new Client($this->getServer()->getUrl()); + $request = $client->get(); + $request->getEventDispatcher()->addListener('request.before_send', function(Event $event) { + $event['request']->setResponse(new Response(200)); + }); + + $multi = new CurlMulti(); + $multi->add($request); + $multi->send(); + $this->assertEquals(0, count($this->getServer()->getReceivedRequests(false))); + } + + public function testDoesNotThrowExceptionsWhenRequestsRecoverWithSuccess() + { + // Attempt a port that 99.9% is not listening + $client = new Client('http://localhost:123'); + $request = $client->get(); + // Ensure it times out quickly if needed + $request->getCurlOptions()->set(CURLOPT_TIMEOUT_MS, 1)->set(CURLOPT_CONNECTTIMEOUT_MS, 1); + + $request->getEventDispatcher()->addListener('request.exception', function(Event $event) use (&$count) { + $event['request']->setResponse(new Response(200)); + }); + + $multi = new CurlMulti(); + $multi->add($request); + $multi->send(); + + // Ensure that the exception was caught, and the response was set manually + $this->assertEquals(200, $request->getResponse()->getStatusCode()); + } + + public function testHardResetReopensMultiHandle() + { + $this->getServer()->enqueue(array( + "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n", + "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n" + )); + + $stream = fopen('php://temp', 'w+'); + $client = new Client($this->getServer()->getUrl()); + $client->getConfig()->set('curl.CURLOPT_VERBOSE', true)->set('curl.CURLOPT_STDERR', $stream); + + $request = $client->get(); + $multi = new CurlMulti(); + $multi->add($request); + $multi->send(); + $multi->reset(true); + $multi->add($request); + $multi->send(); + + rewind($stream); + $this->assertNotContains('Re-using existing connection', stream_get_contents($stream)); + } + + public function testThrowsMeaningfulExceptionsForCurlMultiErrors() + { + $multi = new CurlMulti(); + + // Set the state of the multi object to sending to trigger the exception + $reflector = new \ReflectionMethod('Guzzle\Http\Curl\CurlMulti', 'checkCurlResult'); + $reflector->setAccessible(true); + + // Successful + $reflector->invoke($multi, 0); + + // Known error + try { + $reflector->invoke($multi, CURLM_BAD_HANDLE); + $this->fail('Expected an exception here'); + } catch (CurlException $e) { + $this->assertContains('The passed-in handle is not a valid CURLM handle.', $e->getMessage()); + $this->assertContains('CURLM_BAD_HANDLE', $e->getMessage()); + $this->assertContains(strval(CURLM_BAD_HANDLE), $e->getMessage()); + } + + // Unknown error + try { + $reflector->invoke($multi, 255); + $this->fail('Expected an exception here'); + } catch (CurlException $e) { + $this->assertEquals('Unexpected cURL error: 255', $e->getMessage()); + } + } + + public function testRequestBeforeSendIncludesContentLengthHeaderIfEmptyBody() + { + $this->getServer()->enqueue("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); + $request = new Request('PUT', $this->getServer()->getUrl()); + $that = $this; + $request->getEventDispatcher()->addListener('request.before_send', function ($event) use ($that) { + $that->assertEquals(0, $event['request']->getHeader('Content-Length')); + }); + $this->multi->add($request); + $this->multi->send(); + } + + public function testRemovesConflictingTransferEncodingHeader() + { + $this->getServer()->flush(); + $this->getServer()->enqueue(array( + "HTTP/1.1 200 OK\r\nContent-Length: 4\r\n\r\ntest", + "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n" + )); + $client = new Client($this->getServer()->getUrl()); + $request = $client->put('/', null, fopen($this->getServer()->getUrl(), 'r')); + $request->setHeader('Content-Length', 4); + $request->send(); + $received = $this->getServer()->getReceivedRequests(true); + $this->assertFalse($received[1]->hasHeader('Transfer-Encoding')); + $this->assertEquals(4, (string) $received[1]->getHeader('Content-Length')); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Http/Curl/CurlVersionTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Http/Curl/CurlVersionTest.php new file mode 100644 index 000000000..c7b5ee6e9 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Http/Curl/CurlVersionTest.php @@ -0,0 +1,39 @@ +getProperty('version'); + $refProperty->setAccessible(true); + $refProperty->setValue($instance, array()); + + $this->assertEquals($info, $instance->getAll()); + $this->assertEquals($info, $instance->getAll()); + + $this->assertEquals($info['version'], $instance->get('version')); + $this->assertFalse($instance->get('foo')); + } + + public function testIsSingleton() + { + $refObject = new \ReflectionClass('Guzzle\Http\Curl\CurlVersion'); + $refProperty = $refObject->getProperty('instance'); + $refProperty->setAccessible(true); + $refProperty->setValue(null, null); + + $this->assertInstanceOf('Guzzle\Http\Curl\CurlVersion', CurlVersion::getInstance()); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Http/Curl/RequestMediatorTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Http/Curl/RequestMediatorTest.php new file mode 100644 index 000000000..c69e0c904 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Http/Curl/RequestMediatorTest.php @@ -0,0 +1,67 @@ +events[] = $event; + } + + public function testEmitsEvents() + { + $request = new EntityEnclosingRequest('PUT', 'http://www.example.com'); + $request->setBody('foo'); + $request->setResponse(new Response(200)); + + // Ensure that IO events are emitted + $request->getCurlOptions()->set('emit_io', true); + + // Attach listeners for each event type + $request->getEventDispatcher()->addListener('curl.callback.progress', array($this, 'event')); + $request->getEventDispatcher()->addListener('curl.callback.read', array($this, 'event')); + $request->getEventDispatcher()->addListener('curl.callback.write', array($this, 'event')); + + $mediator = new RequestMediator($request, true); + + $mediator->progress('a', 'b', 'c', 'd'); + $this->assertEquals(1, count($this->events)); + $this->assertEquals('curl.callback.progress', $this->events[0]->getName()); + + $this->assertEquals(3, $mediator->writeResponseBody('foo', 'bar')); + $this->assertEquals(2, count($this->events)); + $this->assertEquals('curl.callback.write', $this->events[1]->getName()); + $this->assertEquals('bar', $this->events[1]['write']); + $this->assertSame($request, $this->events[1]['request']); + + $this->assertEquals('foo', $mediator->readRequestBody('a', 'b', 3)); + $this->assertEquals(3, count($this->events)); + $this->assertEquals('curl.callback.read', $this->events[2]->getName()); + $this->assertEquals('foo', $this->events[2]['read']); + $this->assertSame($request, $this->events[2]['request']); + } + + public function testDoesNotUseRequestResponseBodyWhenNotCustom() + { + $this->getServer()->flush(); + $this->getServer()->enqueue(array( + "HTTP/1.1 307 Foo\r\nLocation: /foo\r\nContent-Length: 2\r\n\r\nHI", + "HTTP/1.1 301 Foo\r\nLocation: /foo\r\nContent-Length: 2\r\n\r\nFI", + "HTTP/1.1 200 OK\r\nContent-Length: 4\r\n\r\ntest", + )); + $client = new Client($this->getServer()->getUrl()); + $response = $client->get()->send(); + $this->assertEquals('test', $response->getBody(true)); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Http/EntityBodyTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Http/EntityBodyTest.php new file mode 100644 index 000000000..124a44da6 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Http/EntityBodyTest.php @@ -0,0 +1,182 @@ +assertEquals('data', (string) $body); + $this->assertEquals(4, $body->getContentLength()); + $this->assertEquals('PHP', $body->getWrapper()); + $this->assertEquals('TEMP', $body->getStreamType()); + + $handle = fopen(__DIR__ . '/../../../../phpunit.xml.dist', 'r'); + if (!$handle) { + $this->fail('Could not open test file'); + } + $body = EntityBody::factory($handle); + $this->assertEquals(__DIR__ . '/../../../../phpunit.xml.dist', $body->getUri()); + $this->assertTrue($body->isLocal()); + $this->assertEquals(__DIR__ . '/../../../../phpunit.xml.dist', $body->getUri()); + $this->assertEquals(filesize(__DIR__ . '/../../../../phpunit.xml.dist'), $body->getContentLength()); + + // make sure that a body will return as the same object + $this->assertTrue($body === EntityBody::factory($body)); + } + + public function testFactoryCreatesTempStreamByDefault() + { + $body = EntityBody::factory(''); + $this->assertEquals('PHP', $body->getWrapper()); + $this->assertEquals('TEMP', $body->getStreamType()); + $body = EntityBody::factory(); + $this->assertEquals('PHP', $body->getWrapper()); + $this->assertEquals('TEMP', $body->getStreamType()); + } + + public function testFactoryCanCreateFromObject() + { + $body = EntityBody::factory(new QueryString(array('foo' => 'bar'))); + $this->assertEquals('foo=bar', (string) $body); + } + + /** + * @expectedException \Guzzle\Common\Exception\InvalidArgumentException + */ + public function testFactoryEnsuresObjectsHaveToStringMethod() + { + EntityBody::factory(new \stdClass('a')); + } + + public function testHandlesCompression() + { + $body = EntityBody::factory('testing 123...testing 123'); + $this->assertFalse($body->getContentEncoding(), '-> getContentEncoding() must initially return FALSE'); + $size = $body->getContentLength(); + $body->compress(); + $this->assertEquals('gzip', $body->getContentEncoding(), '-> getContentEncoding() must return the correct encoding after compressing'); + $this->assertEquals(gzdeflate('testing 123...testing 123'), (string) $body); + $this->assertTrue($body->getContentLength() < $size); + $this->assertTrue($body->uncompress()); + $this->assertEquals('testing 123...testing 123', (string) $body); + $this->assertFalse($body->getContentEncoding(), '-> getContentEncoding() must reset to FALSE'); + + if (in_array('bzip2.*', stream_get_filters())) { + $this->assertTrue($body->compress('bzip2.compress')); + $this->assertEquals('compress', $body->getContentEncoding(), '-> compress() must set \'compress\' as the Content-Encoding'); + } + + $this->assertFalse($body->compress('non-existent'), '-> compress() must return false when a non-existent stream filter is used'); + + // Release the body + unset($body); + + // Use gzip compression on the initial content. This will include a + // gzip header which will need to be stripped when deflating the stream + $body = EntityBody::factory(gzencode('test')); + $this->assertSame($body, $body->setStreamFilterContentEncoding('zlib.deflate')); + $this->assertTrue($body->uncompress('zlib.inflate')); + $this->assertEquals('test', (string) $body); + unset($body); + + // Test using a very long string + $largeString = ''; + for ($i = 0; $i < 25000; $i++) { + $largeString .= chr(rand(33, 126)); + } + $body = EntityBody::factory($largeString); + $this->assertEquals($largeString, (string) $body); + $this->assertTrue($body->compress()); + $this->assertNotEquals($largeString, (string) $body); + $compressed = (string) $body; + $this->assertTrue($body->uncompress()); + $this->assertEquals($largeString, (string) $body); + $this->assertEquals($compressed, gzdeflate($largeString)); + + $body = EntityBody::factory(fopen(__DIR__ . '/../TestData/compress_test', 'w')); + $this->assertFalse($body->compress()); + unset($body); + + unlink(__DIR__ . '/../TestData/compress_test'); + } + + public function testDeterminesContentType() + { + // Test using a string/temp stream + $body = EntityBody::factory('testing 123...testing 123'); + $this->assertNull($body->getContentType()); + + // Use a local file + $body = EntityBody::factory(fopen(__FILE__, 'r')); + $this->assertContains('text/x-', $body->getContentType()); + } + + public function testCreatesMd5Checksum() + { + $body = EntityBody::factory('testing 123...testing 123'); + $this->assertEquals(md5('testing 123...testing 123'), $body->getContentMd5()); + + $server = $this->getServer()->enqueue( + "HTTP/1.1 200 OK" . "\r\n" . + "Content-Length: 3" . "\r\n\r\n" . + "abc" + ); + + $body = EntityBody::factory(fopen($this->getServer()->getUrl(), 'r')); + $this->assertFalse($body->getContentMd5()); + } + + public function testSeeksToOriginalPosAfterMd5() + { + $body = EntityBody::factory('testing 123'); + $body->seek(4); + $this->assertEquals(md5('testing 123'), $body->getContentMd5()); + $this->assertEquals(4, $body->ftell()); + $this->assertEquals('ing 123', $body->read(1000)); + } + + public function testGetTypeFormBodyFactoring() + { + $body = EntityBody::factory(array('key1' => 'val1', 'key2' => 'val2')); + $this->assertEquals('key1=val1&key2=val2', (string) $body); + } + + public function testAllowsCustomRewind() + { + $body = EntityBody::factory('foo'); + $rewound = false; + $body->setRewindFunction(function ($body) use (&$rewound) { + $rewound = true; + return $body->seek(0); + }); + $body->seek(2); + $this->assertTrue($body->rewind()); + $this->assertTrue($rewound); + } + + /** + * @expectedException \Guzzle\Common\Exception\InvalidArgumentException + */ + public function testCustomRewindFunctionMustBeCallable() + { + $body = EntityBody::factory(); + $body->setRewindFunction('foo'); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Http/Exception/CurlExceptionTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Http/Exception/CurlExceptionTest.php new file mode 100644 index 000000000..df3e4b791 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Http/Exception/CurlExceptionTest.php @@ -0,0 +1,27 @@ +assertNull($e->getError()); + $this->assertNull($e->getErrorNo()); + $this->assertSame($e, $e->setError('test', 12)); + $this->assertEquals('test', $e->getError()); + $this->assertEquals(12, $e->getErrorNo()); + + $handle = new CurlHandle(curl_init(), array()); + $e->setCurlHandle($handle); + $this->assertSame($handle, $e->getCurlHandle()); + $handle->close(); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Http/Exception/ExceptionTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Http/Exception/ExceptionTest.php new file mode 100644 index 000000000..12cfd369e --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Http/Exception/ExceptionTest.php @@ -0,0 +1,66 @@ +setRequest($request); + $this->assertEquals($request, $e->getRequest()); + } + + /** + * @covers Guzzle\Http\Exception\BadResponseException + */ + public function testBadResponseException() + { + $e = new BadResponseException('Message'); + $response = new Response(200); + $e->setResponse($response); + $this->assertEquals($response, $e->getResponse()); + } + + /** + * @covers Guzzle\Http\Exception\BadResponseException::factory + */ + public function testCreatesGenericErrorExceptionOnError() + { + $request = new Request('GET', 'http://www.example.com'); + $response = new Response(307); + $e = BadResponseException::factory($request, $response); + $this->assertInstanceOf('Guzzle\Http\Exception\BadResponseException', $e); + } + + /** + * @covers Guzzle\Http\Exception\BadResponseException::factory + */ + public function testCreatesClientErrorExceptionOnClientError() + { + $request = new Request('GET', 'http://www.example.com'); + $response = new Response(404); + $e = BadResponseException::factory($request, $response); + $this->assertInstanceOf('Guzzle\Http\Exception\ClientErrorResponseException', $e); + } + + /** + * @covers Guzzle\Http\Exception\BadResponseException::factory + */ + public function testCreatesServerErrorExceptionOnServerError() + { + $request = new Request('GET', 'http://www.example.com'); + $response = new Response(503); + $e = BadResponseException::factory($request, $response); + $this->assertInstanceOf('Guzzle\Http\Exception\ServerErrorResponseException', $e); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Http/Exception/MultiTransferExceptionTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Http/Exception/MultiTransferExceptionTest.php new file mode 100644 index 000000000..fa4ec2626 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Http/Exception/MultiTransferExceptionTest.php @@ -0,0 +1,51 @@ +addSuccessfulRequest($r1); + $e->addFailedRequest($r2); + $this->assertEquals(array($r1), $e->getSuccessfulRequests()); + $this->assertEquals(array($r2), $e->getSuccessfulRequests()); + $this->assertEquals(array($r1, $r2), $e->getAllRequests()); + $this->assertTrue($e->containsRequest($r1)); + $this->assertTrue($e->containsRequest($r2)); + $this->assertFalse($e->containsRequest(new Request('POST', '/foo'))); + } + + public function testCanSetRequests() + { + $s = array($r1 = new Request('GET', 'http://www.foo.com')); + $f = array($r2 = new Request('GET', 'http://www.foo.com')); + $e = new MultiTransferException(); + $e->setSuccessfulRequests($s); + $e->setFailedRequests($f); + $this->assertEquals(array($r1), $e->getSuccessfulRequests()); + $this->assertEquals(array($r2), $e->getSuccessfulRequests()); + } + + public function testAssociatesExceptionsWithRequests() + { + $r1 = new Request('GET', 'http://www.foo.com'); + $re1 = new \Exception('foo'); + $re2 = new \Exception('bar'); + $e = new MultiTransferException(); + $e->add($re2); + $e->addFailedRequestWithException($r1, $re1); + $this->assertSame($re1, $e->getExceptionForFailedRequest($r1)); + $this->assertNull($e->getExceptionForFailedRequest(new Request('POST', '/foo'))); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Http/IoEmittingEntityBodyTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Http/IoEmittingEntityBodyTest.php new file mode 100644 index 000000000..cd6355f3b --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Http/IoEmittingEntityBodyTest.php @@ -0,0 +1,47 @@ +decorated = EntityBody::factory('hello'); + $this->body = new IoEmittingEntityBody($this->decorated); + } + + public function testEmitsReadEvents() + { + $e = null; + $this->body->getEventDispatcher()->addListener('body.read', function ($event) use (&$e) { + $e = $event; + }); + $this->assertEquals('hel', $this->body->read(3)); + $this->assertEquals('hel', $e['read']); + $this->assertEquals(3, $e['length']); + $this->assertSame($this->body, $e['body']); + } + + public function testEmitsWriteEvents() + { + $e = null; + $this->body->getEventDispatcher()->addListener('body.write', function ($event) use (&$e) { + $e = $event; + }); + $this->body->seek(0, SEEK_END); + $this->assertEquals(5, $this->body->write('there')); + $this->assertEquals('there', $e['write']); + $this->assertEquals(5, $e['result']); + $this->assertSame($this->body, $e['body']); + $this->assertEquals('hellothere', (string) $this->body); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Http/Message/AbstractMessageTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Http/Message/AbstractMessageTest.php new file mode 100644 index 000000000..9447d8c5d --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Http/Message/AbstractMessageTest.php @@ -0,0 +1,136 @@ +mock = $this->getMockForAbstractClass('Guzzle\Http\Message\AbstractMessage'); + } + + public function tearDown() + { + $this->mock = $this->request = null; + } + + public function testGetParams() + { + $request = new Request('GET', 'http://example.com'); + $this->assertInstanceOf('Guzzle\\Common\\Collection', $request->getParams()); + } + + public function testAddHeaders() + { + $this->mock->setHeader('A', 'B'); + + $this->assertEquals($this->mock, $this->mock->addHeaders(array( + 'X-Data' => '123' + ))); + + $this->assertTrue($this->mock->hasHeader('X-Data') !== false); + $this->assertTrue($this->mock->hasHeader('A') !== false); + } + + public function testAllowsHeaderToSetAsHeader() + { + $h = new Header('A', 'B'); + $this->mock->setHeader('A', $h); + $this->assertSame($h, $this->mock->getHeader('A')); + } + + public function testGetHeader() + { + $this->mock->setHeader('Test', '123'); + $this->assertEquals('123', $this->mock->getHeader('Test')); + } + + public function testGetHeaders() + { + $this->assertSame($this->mock, $this->mock->setHeaders(array('a' => 'b', 'c' => 'd'))); + $h = $this->mock->getHeaders(); + $this->assertArrayHasKey('a', $h->toArray()); + $this->assertArrayHasKey('c', $h->toArray()); + $this->assertInstanceOf('Guzzle\Http\Message\Header\HeaderInterface', $h->get('a')); + $this->assertInstanceOf('Guzzle\Http\Message\Header\HeaderInterface', $h->get('c')); + } + + public function testGetHeaderLinesUsesGlue() + { + $this->mock->setHeaders(array('a' => 'b', 'c' => 'd')); + $this->mock->addHeader('a', 'e'); + $this->mock->getHeader('a')->setGlue('!'); + $this->assertEquals(array( + 'a: b! e', + 'c: d' + ), $this->mock->getHeaderLines()); + } + + public function testHasHeader() + { + $this->assertFalse($this->mock->hasHeader('Foo')); + $this->mock->setHeader('Foo', 'Bar'); + $this->assertEquals(true, $this->mock->hasHeader('Foo')); + $this->mock->setHeader('foo', 'yoo'); + $this->assertEquals(true, $this->mock->hasHeader('Foo')); + $this->assertEquals(true, $this->mock->hasHeader('foo')); + $this->assertEquals(false, $this->mock->hasHeader('bar')); + } + + public function testRemoveHeader() + { + $this->mock->setHeader('Foo', 'Bar'); + $this->assertEquals(true, $this->mock->hasHeader('Foo')); + $this->mock->removeHeader('Foo'); + $this->assertFalse($this->mock->hasHeader('Foo')); + } + + public function testReturnsNullWhenHeaderIsNotFound() + { + $this->assertNull($this->mock->getHeader('foo')); + } + + public function testAddingHeadersPreservesOriginalHeaderCase() + { + $this->mock->addHeaders(array( + 'test' => '123', + 'Test' => 'abc' + )); + $this->mock->addHeader('test', '456'); + $this->mock->addHeader('test', '789'); + + $header = $this->mock->getHeader('test'); + $this->assertContains('123', $header->toArray()); + $this->assertContains('456', $header->toArray()); + $this->assertContains('789', $header->toArray()); + $this->assertContains('abc', $header->toArray()); + } + + public function testCanStoreEmptyHeaders() + { + $this->mock->setHeader('Content-Length', 0); + $this->assertTrue($this->mock->hasHeader('Content-Length')); + $this->assertEquals(0, (string) $this->mock->getHeader('Content-Length')); + } + + public function testCanSetCustomHeaderFactory() + { + $f = new Header\HeaderFactory(); + $this->mock->setHeaderFactory($f); + $this->assertSame($f, $this->readAttribute($this->mock, 'headerFactory')); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Http/Message/EntityEnclosingRequestTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Http/Message/EntityEnclosingRequestTest.php new file mode 100644 index 000000000..191b02223 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Http/Message/EntityEnclosingRequestTest.php @@ -0,0 +1,434 @@ +client = new Client(); + } + + public function tearDown() + { + $this->client = null; + } + + public function testConstructorConfiguresRequest() + { + $request = new EntityEnclosingRequest('PUT', 'http://test.com', array( + 'X-Test' => '123' + )); + $request->setBody('Test'); + $this->assertEquals('123', $request->getHeader('X-Test')); + $this->assertNull($request->getHeader('Expect')); + } + + public function testCanSetBodyWithoutOverridingContentType() + { + $request = new EntityEnclosingRequest('PUT', 'http://test.com', array('Content-Type' => 'foooooo')); + $request->setBody('{"a":"b"}'); + $this->assertEquals('foooooo', $request->getHeader('Content-Type')); + } + + public function testRequestIncludesBodyInMessage() + { + + $request = RequestFactory::getInstance()->create('PUT', 'http://www.guzzle-project.com/', null, 'data'); + $this->assertEquals("PUT / HTTP/1.1\r\n" + . "Host: www.guzzle-project.com\r\n" + . "Content-Length: 4\r\n\r\n" + . "data", (string) $request); + } + + public function testRequestIncludesPostBodyInMessageOnlyWhenNoPostFiles() + { + $request = RequestFactory::getInstance()->create('POST', 'http://www.guzzle-project.com/', null, array( + 'foo' => 'bar' + )); + $this->assertEquals("POST / HTTP/1.1\r\n" + . "Host: www.guzzle-project.com\r\n" + . "Content-Type: application/x-www-form-urlencoded; charset=utf-8\r\n\r\n" + . "foo=bar", (string) $request); + + $request = RequestFactory::getInstance()->create('POST', 'http://www.guzzle-project.com/', null, array( + 'foo' => '@' . __FILE__ + )); + $this->assertEquals("POST / HTTP/1.1\r\n" + . "Host: www.guzzle-project.com\r\n" + . "Content-Type: multipart/form-data\r\n" + . "Expect: 100-Continue\r\n\r\n", (string) $request); + } + + public function testAddsPostFieldsAndSetsContentLength() + { + $request = RequestFactory::getInstance()->create('POST', 'http://www.guzzle-project.com/', null, array( + 'data' => '123' + )); + $this->assertEquals("POST / HTTP/1.1\r\n" + . "Host: www.guzzle-project.com\r\n" + . "Content-Type: application/x-www-form-urlencoded; charset=utf-8\r\n\r\n" + . "data=123", (string) $request); + } + + public function testAddsPostFilesAndSetsContentType() + { + $request = RequestFactory::getInstance()->create('POST', 'http://www.test.com/') + ->addPostFiles(array( + 'file' => __FILE__ + ))->addPostFields(array( + 'a' => 'b' + )); + $message = (string) $request; + $this->assertEquals('multipart/form-data', $request->getHeader('Content-Type')); + $this->assertEquals('100-Continue', $request->getHeader('Expect')); + } + + public function testRequestBodyContainsPostFiles() + { + $request = RequestFactory::getInstance()->create('POST', 'http://www.test.com/'); + $request->addPostFields(array( + 'test' => '123' + )); + $this->assertContains("\r\n\r\ntest=123", (string) $request); + } + + public function testRequestBodyAddsContentLength() + { + $request = RequestFactory::getInstance()->create('PUT', 'http://www.test.com/'); + $request->setBody(EntityBody::factory('test')); + $this->assertEquals(4, (string) $request->getHeader('Content-Length')); + $this->assertFalse($request->hasHeader('Transfer-Encoding')); + } + + public function testRequestBodyDoesNotUseContentLengthWhenChunked() + { + $request = RequestFactory::getInstance()->create('PUT', 'http://www.test.com/', array( + 'Transfer-Encoding' => 'chunked' + ), 'test'); + $this->assertNull($request->getHeader('Content-Length')); + $this->assertTrue($request->hasHeader('Transfer-Encoding')); + } + + public function testRequestHasMutableBody() + { + $request = RequestFactory::getInstance()->create('PUT', 'http://www.guzzle-project.com/', null, 'data'); + $body = $request->getBody(); + $this->assertInstanceOf('Guzzle\\Http\\EntityBody', $body); + $this->assertSame($body, $request->getBody()); + + $newBody = EntityBody::factory('foobar'); + $request->setBody($newBody); + $this->assertEquals('foobar', (string) $request->getBody()); + $this->assertSame($newBody, $request->getBody()); + } + + public function testSetPostFields() + { + $request = RequestFactory::getInstance()->create('POST', 'http://www.guzzle-project.com/'); + $this->assertInstanceOf('Guzzle\\Http\\QueryString', $request->getPostFields()); + + $fields = new QueryString(array( + 'a' => 'b' + )); + $request->addPostFields($fields); + $this->assertEquals($fields->getAll(), $request->getPostFields()->getAll()); + $this->assertEquals(array(), $request->getPostFiles()); + } + + public function testSetPostFiles() + { + $request = RequestFactory::getInstance()->create('POST', $this->getServer()->getUrl()) + ->setClient(new Client()) + ->addPostFiles(array(__FILE__)) + ->addPostFields(array( + 'test' => 'abc' + )); + + $request->getCurlOptions()->set('debug', true); + + $this->assertEquals(array( + 'test' => 'abc' + ), $request->getPostFields()->getAll()); + + $files = $request->getPostFiles(); + $post = $files['file'][0]; + $this->assertEquals('file', $post->getFieldName()); + $this->assertContains('text/x-', $post->getContentType()); + $this->assertEquals(__FILE__, $post->getFilename()); + + $this->getServer()->enqueue("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); + $request->send(); + + $this->assertNotNull($request->getHeader('Content-Length')); + $this->assertContains('multipart/form-data; boundary=', (string) $request->getHeader('Content-Type'), '-> cURL must add the boundary'); + } + + /** + * @expectedException Guzzle\Common\Exception\InvalidArgumentException + */ + public function testSetPostFilesThrowsExceptionWhenFileIsNotFound() + { + $request = RequestFactory::getInstance()->create('POST', 'http://www.guzzle-project.com/') + ->addPostFiles(array( + 'file' => 'filenotfound.ini' + )); + } + + /** + * @expectedException Guzzle\Http\Exception\RequestException + */ + public function testThrowsExceptionWhenNonStringsAreAddedToPost() + { + $request = RequestFactory::getInstance()->create('POST', 'http://www.guzzle-project.com/') + ->addPostFile('foo', new \stdClass()); + } + + public function testAllowsContentTypeInPostUploads() + { + $request = RequestFactory::getInstance()->create('POST', 'http://www.guzzle-project.com/') + ->addPostFile('foo', __FILE__, 'text/plain'); + + $this->assertEquals(array( + new PostFile('foo', __FILE__, 'text/plain') + ), $request->getPostFile('foo')); + } + + public function testGuessesContentTypeOfPostUpload() + { + $request = RequestFactory::getInstance()->create('POST', 'http://www.guzzle-project.com/') + ->addPostFile('foo', __FILE__); + $file = $request->getPostFile('foo'); + $this->assertContains('text/x-', $file[0]->getContentType()); + } + + public function testAllowsContentDispositionFieldsInPostUploadsWhenSettingInBulk() + { + $postFile = new PostFile('foo', __FILE__, 'text/x-php'); + $request = RequestFactory::getInstance()->create('POST', 'http://www.guzzle-project.com/') + ->addPostFiles(array('foo' => $postFile)); + + $this->assertEquals(array($postFile), $request->getPostFile('foo')); + } + + public function testPostRequestsUseApplicationXwwwForUrlEncodedForArrays() + { + $request = RequestFactory::getInstance()->create('POST', 'http://www.guzzle-project.com/'); + $request->setPostField('a', 'b'); + $this->assertContains("\r\n\r\na=b", (string) $request); + $this->assertEquals('application/x-www-form-urlencoded; charset=utf-8', $request->getHeader('Content-Type')); + } + + public function testProcessMethodAddsContentType() + { + $request = RequestFactory::getInstance()->create('POST', 'http://www.guzzle-project.com/'); + $request->setPostField('a', 'b'); + $this->assertEquals('application/x-www-form-urlencoded; charset=utf-8', $request->getHeader('Content-Type')); + } + + public function testPostRequestsUseMultipartFormDataWithFiles() + { + $request = RequestFactory::getInstance()->create('POST', 'http://www.guzzle-project.com/'); + $request->addPostFiles(array('file' => __FILE__)); + $this->assertEquals('multipart/form-data', $request->getHeader('Content-Type')); + } + + public function testCanSendMultipleRequestsUsingASingleRequestObject() + { + $this->getServer()->flush(); + $this->getServer()->enqueue(array( + "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n", + "HTTP/1.1 201 Created\r\nContent-Length: 0\r\n\r\n", + )); + + // Send the first request + $request = RequestFactory::getInstance()->create('PUT', $this->getServer()->getUrl()) + ->setBody('test') + ->setClient(new Client()); + $request->send(); + $this->assertEquals(200, $request->getResponse()->getStatusCode()); + + // Send the second request + $request->setBody('abcdefg', 'application/json', false); + $request->send(); + $this->assertEquals(201, $request->getResponse()->getStatusCode()); + + // Ensure that the same request was sent twice with different bodies + $requests = $this->getServer()->getReceivedRequests(true); + $this->assertEquals(2, count($requests)); + $this->assertEquals(4, (string) $requests[0]->getHeader('Content-Length')); + $this->assertEquals(7, (string) $requests[1]->getHeader('Content-Length')); + } + + public function testRemovingPostFieldRebuildsPostFields() + { + $request = new EntityEnclosingRequest('POST', 'http://test.com'); + $request->setPostField('test', 'value'); + $request->removePostField('test'); + $this->assertNull($request->getPostField('test')); + } + + public function testUsesChunkedTransferWhenBodyLengthCannotBeDetermined() + { + $this->getServer()->enqueue("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); + $request = new EntityEnclosingRequest('PUT', 'http://test.com/'); + $request->setBody(fopen($this->getServer()->getUrl(), 'r')); + $this->assertEquals('chunked', $request->getHeader('Transfer-Encoding')); + $this->assertFalse($request->hasHeader('Content-Length')); + } + + /** + * @expectedException \Guzzle\Http\Exception\RequestException + */ + public function testThrowsExceptionWhenContentLengthCannotBeDeterminedAndUsingHttp1() + { + $request = new EntityEnclosingRequest('PUT', 'http://test.com/'); + $this->getServer()->enqueue("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); + $request->setProtocolVersion('1.0'); + $request->setBody(fopen($this->getServer()->getUrl(), 'r')); + } + + public function testAllowsNestedPostData() + { + $request = new EntityEnclosingRequest('POST', 'http://test.com/'); + $request->addPostFields(array( + 'a' => array('b', 'c') + )); + $this->assertEquals(array( + 'a' => array('b', 'c') + ), $request->getPostFields()->getAll()); + } + + public function testAllowsEmptyFields() + { + $request = new EntityEnclosingRequest('POST', 'http://test.com/'); + $request->addPostFields(array( + 'a' => '' + )); + $this->assertEquals(array( + 'a' => '' + ), $request->getPostFields()->getAll()); + } + + /** + * @expectedException \Guzzle\Http\Exception\RequestException + */ + public function testFailsOnInvalidFiles() + { + $request = new EntityEnclosingRequest('POST', 'http://test.com/'); + $request->addPostFiles(array( + 'a' => new \stdClass() + )); + } + + public function testHandlesEmptyStrings() + { + $request = new EntityEnclosingRequest('POST', 'http://test.com/'); + $request->addPostFields(array( + 'a' => '', + 'b' => null, + 'c' => 'Foo' + )); + $this->assertEquals(array( + 'a' => '', + 'b' => null, + 'c' => 'Foo' + ), $request->getPostFields()->getAll()); + } + + public function testHoldsPostFiles() + { + $request = new EntityEnclosingRequest('POST', 'http://test.com/'); + $request->addPostFile('foo', __FILE__); + $request->addPostFile(new PostFile('foo', __FILE__)); + + $this->assertArrayHasKey('foo', $request->getPostFiles()); + $foo = $request->getPostFile('foo'); + $this->assertEquals(2, count($foo)); + $this->assertEquals(__FILE__, $foo[0]->getFilename()); + $this->assertEquals(__FILE__, $foo[1]->getFilename()); + + $request->removePostFile('foo'); + $this->assertEquals(array(), $request->getPostFiles()); + } + + public function testAllowsAtPrefixWhenAddingPostFiles() + { + $request = new EntityEnclosingRequest('POST', 'http://test.com/'); + $request->addPostFiles(array( + 'foo' => '@' . __FILE__ + )); + $foo = $request->getPostFile('foo'); + $this->assertEquals(__FILE__, $foo[0]->getFilename()); + } + + public function testSetStateToTransferWithEmptyBodySetsContentLengthToZero() + { + $request = new EntityEnclosingRequest('POST', 'http://test.com/'); + $request->setState($request::STATE_TRANSFER); + $this->assertEquals('0', (string) $request->getHeader('Content-Length')); + } + + public function testSettingExpectHeaderCutoffChangesRequest() + { + $request = new EntityEnclosingRequest('PUT', 'http://test.com/'); + $request->setHeader('Expect', '100-Continue'); + $request->setExpectHeaderCutoff(false); + $this->assertNull($request->getHeader('Expect')); + // There is not body, so remove the expect header + $request->setHeader('Expect', '100-Continue'); + $request->setExpectHeaderCutoff(10); + $this->assertNull($request->getHeader('Expect')); + // The size is less than the cutoff + $request->setBody('foo'); + $this->assertNull($request->getHeader('Expect')); + // The size is greater than the cutoff + $request->setBody('foobazbarbamboo'); + $this->assertNotNull($request->getHeader('Expect')); + } + + public function testStrictRedirectsCanBeSpecifiedOnEntityEnclosingRequests() + { + $request = new EntityEnclosingRequest('PUT', 'http://test.com/'); + $request->configureRedirects(true); + $this->assertTrue($request->getParams()->get(RedirectPlugin::STRICT_REDIRECTS)); + } + + public function testCanDisableRedirects() + { + $request = new EntityEnclosingRequest('PUT', 'http://test.com/'); + $request->configureRedirects(false, false); + $this->assertTrue($request->getParams()->get(RedirectPlugin::DISABLE)); + } + + public function testSetsContentTypeWhenSettingBodyByGuessingFromEntityBody() + { + $request = new EntityEnclosingRequest('PUT', 'http://test.com/foo'); + $request->setBody(EntityBody::factory(fopen(__FILE__, 'r'))); + $this->assertEquals('text/x-php', (string) $request->getHeader('Content-Type')); + } + + public function testDoesNotCloneBody() + { + $request = new EntityEnclosingRequest('PUT', 'http://test.com/foo'); + $request->setBody('test'); + $newRequest = clone $request; + $newRequest->setBody('foo'); + $this->assertInternalType('string', (string) $request->getBody()); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Http/Message/Header/HeaderFactoryTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Http/Message/Header/HeaderFactoryTest.php new file mode 100644 index 000000000..62ca5559d --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Http/Message/Header/HeaderFactoryTest.php @@ -0,0 +1,29 @@ +createHeader('Foo', 'Bar'); + $this->assertInstanceOf('Guzzle\Http\Message\Header', $h); + $this->assertEquals('Foo', $h->getName()); + $this->assertEquals('Bar', (string) $h); + } + + public function testCreatesSpecificHeaders() + { + $f = new HeaderFactory(); + $h = $f->createHeader('Link', '; rel="test"'); + $this->assertInstanceOf('Guzzle\Http\Message\Header\Link', $h); + $this->assertEquals('Link', $h->getName()); + $this->assertEquals('; rel="test"', (string) $h); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Http/Message/Header/LinkTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Http/Message/Header/LinkTest.php new file mode 100644 index 000000000..c834d1016 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Http/Message/Header/LinkTest.php @@ -0,0 +1,63 @@ +; rel=front; type="image/jpeg", ; rel=back; type="image/jpeg", ; rel=side; type="image/jpeg"'); + $links = $link->getLinks(); + $this->assertEquals(array( + array( + 'rel' => 'front', + 'type' => 'image/jpeg', + 'url' => 'http:/.../front.jpeg', + ), + array( + 'rel' => 'back', + 'type' => 'image/jpeg', + 'url' => 'http://.../back.jpeg', + ), + array( + 'rel' => 'side', + 'type' => 'image/jpeg', + 'url' => 'http://.../side.jpeg?test=1' + ) + ), $links); + + $this->assertEquals(array( + 'rel' => 'back', + 'type' => 'image/jpeg', + 'url' => 'http://.../back.jpeg', + ), $link->getLink('back')); + + $this->assertTrue($link->hasLink('front')); + $this->assertFalse($link->hasLink('foo')); + } + + public function testCanAddLink() + { + $link = new Link('Link', '; rel=a; type="image/jpeg"'); + $link->addLink('http://test.com', 'test', array('foo' => 'bar')); + $this->assertEquals( + '; rel=a; type="image/jpeg", ; rel="test"; foo="bar"', + (string) $link + ); + } + + public function testCanParseLinksWithCommas() + { + $link = new Link('Link', '; rel="previous"; title="start, index"'); + $this->assertEquals(array( + array( + 'rel' => 'previous', + 'title' => 'start, index', + 'url' => 'http://example.com/TheBook/chapter1', + ) + ), $link->getLinks()); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Http/Message/HeaderComparison.php b/guzzle/guzzle/tests/Guzzle/Tests/Http/Message/HeaderComparison.php new file mode 100644 index 000000000..a3f511bfc --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Http/Message/HeaderComparison.php @@ -0,0 +1,135 @@ +toArray(); + } + + foreach ($filteredHeaders as $k => $v) { + if ($k[0] == '_') { + // This header should be ignored + $ignore[] = str_replace('_', '', $k); + } elseif ($k[0] == '!') { + // This header must not be present + $absent[] = str_replace('!', '', $k); + } else { + $expected[$k] = $v; + } + } + + return $this->compareArray($expected, $actualHeaders, $ignore, $absent); + } + + /** + * Check if an array of HTTP headers matches another array of HTTP headers while taking * into account as a wildcard + * + * @param array $expected Expected HTTP headers (allows wildcard values) + * @param array|Collection $actual Actual HTTP header array + * @param array $ignore Headers to ignore from the comparison + * @param array $absent Array of headers that must not be present + * + * @return array|bool Returns an array of the differences or FALSE if none + */ + public function compareArray(array $expected, $actual, array $ignore = array(), array $absent = array()) + { + $differences = array(); + + // Add information about headers that were present but weren't supposed to be + foreach ($absent as $header) { + if ($this->hasKey($header, $actual)) { + $differences["++ {$header}"] = $actual[$header]; + unset($actual[$header]); + } + } + + // Check if expected headers are missing + foreach ($expected as $header => $value) { + if (!$this->hasKey($header, $actual)) { + $differences["- {$header}"] = $value; + } + } + + // Flip the ignore array so it works with the case insensitive helper + $ignore = array_flip($ignore); + // Allow case-insensitive comparisons in wildcards + $expected = array_change_key_case($expected); + + // Compare the expected and actual HTTP headers in no particular order + foreach ($actual as $key => $value) { + + // If this is to be ignored, the skip it + if ($this->hasKey($key, $ignore)) { + continue; + } + + // If the header was not expected + if (!$this->hasKey($key, $expected)) { + $differences["+ {$key}"] = $value; + continue; + } + + // Check values and take wildcards into account + $lkey = strtolower($key); + $pos = is_string($expected[$lkey]) ? strpos($expected[$lkey], '*') : false; + + foreach ((array) $actual[$key] as $v) { + if (($pos === false && $v != $expected[$lkey]) || $pos > 0 && substr($v, 0, $pos) != substr($expected[$lkey], 0, $pos)) { + $differences[$key] = "{$value} != {$expected[$lkey]}"; + } + } + } + + return empty($differences) ? false : $differences; + } + + /** + * Case insensitive check if an array have a key + * + * @param string $key Key to check + * @param array $array Array to check + * + * @return bool + */ + protected function hasKey($key, $array) + { + if ($array instanceof Collection) { + $keys = $array->getKeys(); + } else { + $keys = array_keys($array); + } + + foreach ($keys as $k) { + if (!strcasecmp($k, $key)) { + return true; + } + } + + return false; + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Http/Message/HeaderComparisonTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Http/Message/HeaderComparisonTest.php new file mode 100644 index 000000000..86c4fe866 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Http/Message/HeaderComparisonTest.php @@ -0,0 +1,115 @@ + 'Foo' + ), array( + 'Content-Length' => 'Foo' + ), false), + + // Missing header + array(array( + 'X-Foo' => 'Bar' + ), array(), array( + '- X-Foo' => 'Bar' + )), + + // Extra headers is present + array(array( + 'X-Foo' => 'Bar' + ), array( + 'X-Foo' => 'Bar', + 'X-Baz' => 'Jar' + ), array( + '+ X-Baz' => 'Jar' + )), + + // Header is present but must be absent + array(array( + '!X-Foo' => '*' + ), array( + 'X-Foo' => 'Bar' + ), array( + '++ X-Foo' => 'Bar' + )), + + // Different values + array(array( + 'X-Foo' => 'Bar' + ), array( + 'X-Foo' => 'Baz' + ), array( + 'X-Foo' => 'Baz != Bar' + )), + + // Wildcard search passes + array(array( + 'X-Foo' => '*' + ), array( + 'X-Foo' => 'Bar' + ), false), + + // Wildcard search fails + array(array( + 'X-Foo' => '*' + ), array(), array( + '- X-Foo' => '*' + )), + + // Ignore extra header if present + array(array( + 'X-Foo' => '*', + '_X-Bar' => '*', + ), array( + 'X-Foo' => 'Baz', + 'X-Bar' => 'Jar' + ), false), + + // Ignore extra header if present and is not + array(array( + 'X-Foo' => '*', + '_X-Bar' => '*', + ), array( + 'X-Foo' => 'Baz' + ), false), + + // Case insensitive + array(array( + 'X-Foo' => '*', + '_X-Bar' => '*', + ), array( + 'x-foo' => 'Baz', + 'x-BAR' => 'baz' + ), false), + + // Case insensitive with collection + array(array( + 'X-Foo' => '*', + '_X-Bar' => '*', + ), new Collection(array( + 'x-foo' => 'Baz', + 'x-BAR' => 'baz' + )), false), + ); + } + + /** + * @dataProvider filterProvider + */ + public function testComparesHeaders($filters, $headers, $result) + { + $compare = new HeaderComparison(); + $this->assertEquals($result, $compare->compare($filters, $headers)); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Http/Message/HeaderTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Http/Message/HeaderTest.php new file mode 100644 index 000000000..c7502349f --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Http/Message/HeaderTest.php @@ -0,0 +1,162 @@ + array('foo', 'Foo'), + 'Zoo' => 'bar', + ); + + public function testStoresHeaderName() + { + $i = new Header('Zoo', $this->test); + $this->assertEquals('Zoo', $i->getName()); + } + + public function testConvertsToString() + { + $i = new Header('Zoo', $this->test); + $this->assertEquals('foo, Foo, bar', (string) $i); + $i->setGlue(';'); + $this->assertEquals('foo; Foo; bar', (string) $i); + } + + public function testNormalizesGluedHeaders() + { + $h = new Header('Zoo', array('foo, Faz', 'bar')); + $result = $h->normalize(true)->toArray(); + natsort($result); + $this->assertEquals(array('bar', 'foo', 'Faz'), $result); + } + + public function testCanSearchForValues() + { + $h = new Header('Zoo', $this->test); + $this->assertTrue($h->hasValue('foo')); + $this->assertTrue($h->hasValue('Foo')); + $this->assertTrue($h->hasValue('bar')); + $this->assertFalse($h->hasValue('moo')); + $this->assertFalse($h->hasValue('FoO')); + } + + public function testIsCountable() + { + $h = new Header('Zoo', $this->test); + $this->assertEquals(3, count($h)); + } + + public function testCanBeIterated() + { + $h = new Header('Zoo', $this->test); + $results = array(); + foreach ($h as $key => $value) { + $results[$key] = $value; + } + $this->assertEquals(array( + 'foo', 'Foo', 'bar' + ), $results); + } + + public function testAllowsFalseyValues() + { + // Allows 0 + $h = new Header('Foo', 0, ';'); + $this->assertEquals('0', (string) $h); + $this->assertEquals(1, count($h)); + $this->assertEquals(';', $h->getGlue()); + + // Does not add a null header by default + $h = new Header('Foo'); + $this->assertEquals('', (string) $h); + $this->assertEquals(0, count($h)); + + // Allows null array for a single null header + $h = new Header('Foo', array(null)); + $this->assertEquals('', (string) $h); + + // Allows empty string + $h = new Header('Foo', ''); + $this->assertEquals('', (string) $h); + $this->assertEquals(1, count($h)); + $this->assertEquals(1, count($h->normalize()->toArray())); + } + + public function testCanRemoveValues() + { + $h = new Header('Foo', array('Foo', 'baz', 'bar')); + $h->removeValue('bar'); + $this->assertTrue($h->hasValue('Foo')); + $this->assertFalse($h->hasValue('bar')); + $this->assertTrue($h->hasValue('baz')); + } + + public function testAllowsArrayInConstructor() + { + $h = new Header('Foo', array('Testing', '123', 'Foo=baz')); + $this->assertEquals(array('Testing', '123', 'Foo=baz'), $h->toArray()); + } + + public function parseParamsProvider() + { + $res1 = array( + array( + '' => '', + 'rel' => 'front', + 'type' => 'image/jpeg', + ), + array( + '' => '', + 'rel' => 'back', + 'type' => 'image/jpeg', + ), + ); + + return array( + array( + '; rel="front"; type="image/jpeg", ; rel=back; type="image/jpeg"', + $res1 + ), + array( + '; rel="front"; type="image/jpeg",; rel=back; type="image/jpeg"', + $res1 + ), + array( + 'foo="baz"; bar=123, boo, test="123", foobar="foo;bar"', + array( + array('foo' => 'baz', 'bar' => '123'), + array('boo' => ''), + array('test' => '123'), + array('foobar' => 'foo;bar') + ) + ), + array( + '; rel="side"; type="image/jpeg",; rel=side; type="image/jpeg"', + array( + array('' => '', 'rel' => 'side', 'type' => 'image/jpeg'), + array('' => '', 'rel' => 'side', 'type' => 'image/jpeg') + ) + ), + array( + '', + array() + ) + ); + } + + /** + * @dataProvider parseParamsProvider + */ + public function testParseParams($header, $result) + { + $response = new Response(200, array('Link' => $header)); + $this->assertEquals($result, $response->getHeader('Link')->parseParams()); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Http/Message/PostFileTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Http/Message/PostFileTest.php new file mode 100644 index 000000000..be048cb95 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Http/Message/PostFileTest.php @@ -0,0 +1,88 @@ +assertEquals('foo', $file->getFieldName()); + $this->assertEquals(__FILE__, $file->getFilename()); + $this->assertEquals('boo', $file->getPostName()); + $this->assertEquals('x-foo', $file->getContentType()); + } + + public function testRemovesLeadingAtSymbolFromPath() + { + $file = new PostFile('foo', '@' . __FILE__); + $this->assertEquals(__FILE__, $file->getFilename()); + } + + /** + * @expectedException Guzzle\Common\Exception\InvalidArgumentException + */ + public function testEnsuresFileIsReadable() + { + $file = new PostFile('foo', '/foo/baz/bar'); + } + + public function testCanChangeContentType() + { + $file = new PostFile('foo', '@' . __FILE__); + $file->setContentType('Boo'); + $this->assertEquals('Boo', $file->getContentType()); + } + + public function testCanChangeFieldName() + { + $file = new PostFile('foo', '@' . __FILE__); + $file->setFieldName('Boo'); + $this->assertEquals('Boo', $file->getFieldName()); + } + + public function testReturnsCurlValueString() + { + $file = new PostFile('foo', __FILE__); + if (version_compare(phpversion(), '5.5.0', '<')) { + $this->assertContains('@' . __FILE__ . ';filename=PostFileTest.php;type=text/x-', $file->getCurlValue()); + } else { + $c = $file->getCurlValue(); + $this->assertEquals(__FILE__, $c->getFilename()); + $this->assertEquals('PostFileTest.php', $c->getPostFilename()); + $this->assertContains('text/x-', $c->getMimeType()); + } + } + + public function testReturnsCurlValueStringAndPostname() + { + $file = new PostFile('foo', __FILE__, null, 'NewPostFileTest.php'); + if (version_compare(phpversion(), '5.5.0', '<')) { + $this->assertContains('@' . __FILE__ . ';filename=NewPostFileTest.php;type=text/x-', $file->getCurlValue()); + } else { + $c = $file->getCurlValue(); + $this->assertEquals(__FILE__, $c->getFilename()); + $this->assertEquals('NewPostFileTest.php', $c->getPostFilename()); + $this->assertContains('text/x-', $c->getMimeType()); + } + } + + public function testContentDispositionFilePathIsStripped() + { + $this->getServer()->flush(); + $client = new Client($this->getServer()->getUrl()); + $this->getServer()->enqueue("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); + $request = $client->post()->addPostFile('file', __FILE__); + $request->send(); + $requests = $this->getServer()->getReceivedRequests(false); + $this->assertContains('POST / HTTP/1.1', $requests[0]); + $this->assertContains('Content-Disposition: form-data; name="file"; filename="PostFileTest.php"', $requests[0]); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Http/Message/RequestFactoryTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Http/Message/RequestFactoryTest.php new file mode 100644 index 000000000..d43e0899c --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Http/Message/RequestFactoryTest.php @@ -0,0 +1,613 @@ +assertSame($factory, RequestFactory::getInstance()); + } + + public function testCreatesNewGetRequests() + { + $request = RequestFactory::getInstance()->create('GET', 'http://www.google.com/'); + $this->assertInstanceOf('Guzzle\\Http\\Message\\MessageInterface', $request); + $this->assertInstanceOf('Guzzle\\Http\\Message\\RequestInterface', $request); + $this->assertInstanceOf('Guzzle\\Http\\Message\\Request', $request); + $this->assertEquals('GET', $request->getMethod()); + $this->assertEquals('http', $request->getScheme()); + $this->assertEquals('http://www.google.com/', $request->getUrl()); + $this->assertEquals('www.google.com', $request->getHost()); + $this->assertEquals('/', $request->getPath()); + $this->assertEquals('/', $request->getResource()); + + // Create a GET request with a custom receiving body + $this->getServer()->enqueue("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); + $b = EntityBody::factory(); + $request = RequestFactory::getInstance()->create('GET', $this->getServer()->getUrl(), null, $b); + $request->setClient(new Client()); + $response = $request->send(); + $this->assertSame($b, $response->getBody()); + } + + public function testCreatesPutRequests() + { + // Test using a string + $request = RequestFactory::getInstance()->create('PUT', 'http://www.google.com/path?q=1&v=2', null, 'Data'); + $this->assertInstanceOf('Guzzle\\Http\\Message\\EntityEnclosingRequest', $request); + $this->assertEquals('PUT', $request->getMethod()); + $this->assertEquals('http', $request->getScheme()); + $this->assertEquals('http://www.google.com/path?q=1&v=2', $request->getUrl()); + $this->assertEquals('www.google.com', $request->getHost()); + $this->assertEquals('/path', $request->getPath()); + $this->assertEquals('/path?q=1&v=2', $request->getResource()); + $this->assertInstanceOf('Guzzle\\Http\\EntityBody', $request->getBody()); + $this->assertEquals('Data', (string) $request->getBody()); + unset($request); + + // Test using an EntityBody + $request = RequestFactory::getInstance()->create('PUT', 'http://www.google.com/path?q=1&v=2', null, EntityBody::factory('Data')); + $this->assertInstanceOf('Guzzle\\Http\\Message\\EntityEnclosingRequest', $request); + $this->assertEquals('Data', (string) $request->getBody()); + + // Test using a resource + $resource = fopen('php://temp', 'w+'); + fwrite($resource, 'Data'); + $request = RequestFactory::getInstance()->create('PUT', 'http://www.google.com/path?q=1&v=2', null, $resource); + $this->assertInstanceOf('Guzzle\\Http\\Message\\EntityEnclosingRequest', $request); + $this->assertEquals('Data', (string) $request->getBody()); + + // Test using an object that can be cast as a string + $request = RequestFactory::getInstance()->create('PUT', 'http://www.google.com/path?q=1&v=2', null, Url::factory('http://www.example.com/')); + $this->assertInstanceOf('Guzzle\\Http\\Message\\EntityEnclosingRequest', $request); + $this->assertEquals('http://www.example.com/', (string) $request->getBody()); + } + + public function testCreatesHeadAndDeleteRequests() + { + $request = RequestFactory::getInstance()->create('DELETE', 'http://www.test.com/'); + $this->assertEquals('DELETE', $request->getMethod()); + $request = RequestFactory::getInstance()->create('HEAD', 'http://www.test.com/'); + $this->assertEquals('HEAD', $request->getMethod()); + } + + public function testCreatesOptionsRequests() + { + $request = RequestFactory::getInstance()->create('OPTIONS', 'http://www.example.com/'); + $this->assertEquals('OPTIONS', $request->getMethod()); + $this->assertInstanceOf('Guzzle\\Http\\Message\\Request', $request); + } + + public function testCreatesNewPutRequestWithBody() + { + $request = RequestFactory::getInstance()->create('PUT', 'http://www.google.com/path?q=1&v=2', null, 'Data'); + $this->assertEquals('Data', (string) $request->getBody()); + } + + public function testCreatesNewPostRequestWithFields() + { + // Use an array + $request = RequestFactory::getInstance()->create('POST', 'http://www.google.com/path?q=1&v=2', null, array( + 'a' => 'b' + )); + $this->assertEquals(array('a' => 'b'), $request->getPostFields()->getAll()); + unset($request); + + // Use a collection + $request = RequestFactory::getInstance()->create('POST', 'http://www.google.com/path?q=1&v=2', null, new Collection(array( + 'a' => 'b' + ))); + $this->assertEquals(array('a' => 'b'), $request->getPostFields()->getAll()); + + // Use a QueryString + $request = RequestFactory::getInstance()->create('POST', 'http://www.google.com/path?q=1&v=2', null, new QueryString(array( + 'a' => 'b' + ))); + $this->assertEquals(array('a' => 'b'), $request->getPostFields()->getAll()); + + $request = RequestFactory::getInstance()->create('POST', 'http://www.test.com/', null, array( + 'a' => 'b', + 'file' => '@' . __FILE__ + )); + + $this->assertEquals(array( + 'a' => 'b' + ), $request->getPostFields()->getAll()); + + $files = $request->getPostFiles(); + $this->assertInstanceOf('Guzzle\Http\Message\PostFile', $files['file'][0]); + } + + public function testCreatesFromParts() + { + $parts = parse_url('http://michael:123@www.google.com:8080/path?q=1&v=2'); + + $request = RequestFactory::getInstance()->fromParts('PUT', $parts, null, 'Data'); + $this->assertInstanceOf('Guzzle\\Http\\Message\\EntityEnclosingRequest', $request); + $this->assertEquals('PUT', $request->getMethod()); + $this->assertEquals('http', $request->getScheme()); + $this->assertEquals('http://www.google.com:8080/path?q=1&v=2', $request->getUrl()); + $this->assertEquals('www.google.com', $request->getHost()); + $this->assertEquals('www.google.com:8080', $request->getHeader('Host')); + $this->assertEquals('/path', $request->getPath()); + $this->assertEquals('/path?q=1&v=2', $request->getResource()); + $this->assertInstanceOf('Guzzle\\Http\\EntityBody', $request->getBody()); + $this->assertEquals('Data', (string) $request->getBody()); + $this->assertEquals('michael', $request->getUsername()); + $this->assertEquals('123', $request->getPassword()); + $this->assertEquals('8080', $request->getPort()); + $this->assertEquals(array( + 'scheme' => 'http', + 'host' => 'www.google.com', + 'port' => 8080, + 'path' => '/path', + 'query' => 'q=1&v=2', + ), parse_url($request->getUrl())); + } + + public function testCreatesFromMessage() + { + $auth = base64_encode('michael:123'); + $message = "PUT /path?q=1&v=2 HTTP/1.1\r\nHost: www.google.com:8080\r\nContent-Length: 4\r\nAuthorization: Basic {$auth}\r\n\r\nData"; + $request = RequestFactory::getInstance()->fromMessage($message); + $this->assertInstanceOf('Guzzle\\Http\\Message\\EntityEnclosingRequest', $request); + $this->assertEquals('PUT', $request->getMethod()); + $this->assertEquals('http', $request->getScheme()); + $this->assertEquals('http://www.google.com:8080/path?q=1&v=2', $request->getUrl()); + $this->assertEquals('www.google.com', $request->getHost()); + $this->assertEquals('www.google.com:8080', $request->getHeader('Host')); + $this->assertEquals('/path', $request->getPath()); + $this->assertEquals('/path?q=1&v=2', $request->getResource()); + $this->assertInstanceOf('Guzzle\\Http\\EntityBody', $request->getBody()); + $this->assertEquals('Data', (string) $request->getBody()); + $this->assertEquals("Basic {$auth}", (string) $request->getHeader('Authorization')); + $this->assertEquals('8080', $request->getPort()); + + // Test passing a blank message returns false + $this->assertFalse($request = RequestFactory::getInstance()->fromMessage('')); + + // Test passing a url with no port + $message = "PUT /path?q=1&v=2 HTTP/1.1\r\nHost: www.google.com\r\nContent-Length: 4\r\nAuthorization: Basic {$auth}\r\n\r\nData"; + $request = RequestFactory::getInstance()->fromMessage($message); + $this->assertInstanceOf('Guzzle\\Http\\Message\\EntityEnclosingRequest', $request); + $this->assertEquals('PUT', $request->getMethod()); + $this->assertEquals('http', $request->getScheme()); + $this->assertEquals('http://www.google.com/path?q=1&v=2', $request->getUrl()); + $this->assertEquals('www.google.com', $request->getHost()); + $this->assertEquals('/path', $request->getPath()); + $this->assertEquals('/path?q=1&v=2', $request->getResource()); + $this->assertInstanceOf('Guzzle\\Http\\EntityBody', $request->getBody()); + $this->assertEquals('Data', (string) $request->getBody()); + $this->assertEquals("Basic {$auth}", (string) $request->getHeader('Authorization')); + $this->assertEquals(80, $request->getPort()); + } + + public function testCreatesNewTraceRequest() + { + $request = RequestFactory::getInstance()->create('TRACE', 'http://www.google.com/'); + $this->assertFalse($request instanceof \Guzzle\Http\Message\EntityEnclosingRequest); + $this->assertEquals('TRACE', $request->getMethod()); + } + + public function testCreatesProperTransferEncodingRequests() + { + $request = RequestFactory::getInstance()->create('PUT', 'http://www.google.com/', array( + 'Transfer-Encoding' => 'chunked' + ), 'hello'); + $this->assertEquals('chunked', $request->getHeader('Transfer-Encoding')); + $this->assertFalse($request->hasHeader('Content-Length')); + } + + public function testProperlyDealsWithDuplicateHeaders() + { + $parser = new MessageParser(); + + $message = "POST / http/1.1\r\n" + . "DATE:Mon, 09 Sep 2011 23:36:00 GMT\r\n" + . "host:host.foo.com\r\n" + . "ZOO:abc\r\n" + . "ZOO:123\r\n" + . "ZOO:HI\r\n" + . "zoo:456\r\n\r\n"; + + $parts = $parser->parseRequest($message); + $this->assertEquals(array ( + 'DATE' => 'Mon, 09 Sep 2011 23:36:00 GMT', + 'host' => 'host.foo.com', + 'ZOO' => array('abc', '123', 'HI'), + 'zoo' => '456', + ), $parts['headers']); + + $request = RequestFactory::getInstance()->fromMessage($message); + + $this->assertEquals(array( + 'abc', '123', 'HI', '456' + ), $request->getHeader('zoo')->toArray()); + } + + public function testCreatesHttpMessagesWithBodiesAndNormalizesLineEndings() + { + $message = "POST / http/1.1\r\n" + . "Content-Type:application/x-www-form-urlencoded; charset=utf8\r\n" + . "Date:Mon, 09 Sep 2011 23:36:00 GMT\r\n" + . "Host:host.foo.com\r\n\r\n" + . "foo=bar"; + + $request = RequestFactory::getInstance()->fromMessage($message); + $this->assertEquals('application/x-www-form-urlencoded; charset=utf8', (string) $request->getHeader('Content-Type')); + $this->assertEquals('foo=bar', (string) $request->getBody()); + + $message = "POST / http/1.1\n" + . "Content-Type:application/x-www-form-urlencoded; charset=utf8\n" + . "Date:Mon, 09 Sep 2011 23:36:00 GMT\n" + . "Host:host.foo.com\n\n" + . "foo=bar"; + $request = RequestFactory::getInstance()->fromMessage($message); + $this->assertEquals('foo=bar', (string) $request->getBody()); + + $message = "PUT / HTTP/1.1\r\nContent-Length: 0\r\n\r\n"; + $request = RequestFactory::getInstance()->fromMessage($message); + $this->assertTrue($request->hasHeader('Content-Length')); + $this->assertEquals(0, (string) $request->getHeader('Content-Length')); + } + + public function testBugPathIncorrectlyHandled() + { + $message = "POST /foo\r\n\r\nBODY"; + $request = RequestFactory::getInstance()->fromMessage($message); + $this->assertSame('POST', $request->getMethod()); + $this->assertSame('/foo', $request->getPath()); + $this->assertSame('BODY', (string) $request->getBody()); + } + + public function testHandlesChunkedTransferEncoding() + { + $request = RequestFactory::getInstance()->create('PUT', 'http://www.foo.com/', array( + 'Transfer-Encoding' => 'chunked' + ), 'Test'); + $this->assertFalse($request->hasHeader('Content-Length')); + $this->assertEquals('chunked', $request->getHeader('Transfer-Encoding')); + + $request = RequestFactory::getInstance()->create('POST', 'http://www.foo.com/', array( + 'transfer-encoding' => 'chunked' + ), array( + 'foo' => 'bar' + )); + + $this->assertFalse($request->hasHeader('Content-Length')); + $this->assertEquals('chunked', $request->getHeader('Transfer-Encoding')); + } + + public function testClonesRequestsWithMethodWithoutClient() + { + $f = RequestFactory::getInstance(); + $request = $f->create('GET', 'http://www.test.com', array('X-Foo' => 'Bar')); + $request->getParams()->replace(array('test' => '123')); + $request->getCurlOptions()->set('foo', 'bar'); + $cloned = $f->cloneRequestWithMethod($request, 'PUT'); + $this->assertEquals('PUT', $cloned->getMethod()); + $this->assertEquals('Bar', (string) $cloned->getHeader('X-Foo')); + $this->assertEquals('http://www.test.com', $cloned->getUrl()); + // Ensure params are cloned and cleaned up + $this->assertEquals(1, count($cloned->getParams()->getAll())); + $this->assertEquals('123', $cloned->getParams()->get('test')); + // Ensure curl options are cloned + $this->assertEquals('bar', $cloned->getCurlOptions()->get('foo')); + // Ensure event dispatcher is cloned + $this->assertNotSame($request->getEventDispatcher(), $cloned->getEventDispatcher()); + } + + public function testClonesRequestsWithMethodWithClient() + { + $f = RequestFactory::getInstance(); + $client = new Client(); + $request = $client->put('http://www.test.com', array('Content-Length' => 4), 'test'); + $cloned = $f->cloneRequestWithMethod($request, 'GET'); + $this->assertEquals('GET', $cloned->getMethod()); + $this->assertNull($cloned->getHeader('Content-Length')); + $this->assertEquals('http://www.test.com', $cloned->getUrl()); + $this->assertSame($request->getClient(), $cloned->getClient()); + } + + public function testClonesRequestsWithMethodWithClientWithEntityEnclosingChange() + { + $f = RequestFactory::getInstance(); + $client = new Client(); + $request = $client->put('http://www.test.com', array('Content-Length' => 4), 'test'); + $cloned = $f->cloneRequestWithMethod($request, 'POST'); + $this->assertEquals('POST', $cloned->getMethod()); + $this->assertEquals('test', (string) $cloned->getBody()); + } + + public function testCanDisableRedirects() + { + $this->getServer()->enqueue(array( + "HTTP/1.1 307\r\nLocation: " . $this->getServer()->getUrl() . "\r\nContent-Length: 0\r\n\r\n" + )); + $client = new Client($this->getServer()->getUrl()); + $response = $client->get('/', array(), array('allow_redirects' => false))->send(); + $this->assertEquals(307, $response->getStatusCode()); + } + + public function testCanAddCookies() + { + $client = new Client($this->getServer()->getUrl()); + $request = $client->get('/', array(), array('cookies' => array('Foo' => 'Bar'))); + $this->assertEquals('Bar', $request->getCookie('Foo')); + } + + public function testCanAddQueryString() + { + $request = RequestFactory::getInstance()->create('GET', 'http://foo.com', array(), null, array( + 'query' => array('Foo' => 'Bar') + )); + $this->assertEquals('Bar', $request->getQuery()->get('Foo')); + } + + public function testCanSetDefaultQueryString() + { + $request = new Request('GET', 'http://www.foo.com?test=abc'); + RequestFactory::getInstance()->applyOptions($request, array( + 'query' => array('test' => '123', 'other' => 't123') + ), RequestFactory::OPTIONS_AS_DEFAULTS); + $this->assertEquals('abc', $request->getQuery()->get('test')); + $this->assertEquals('t123', $request->getQuery()->get('other')); + } + + public function testCanAddBasicAuth() + { + $request = RequestFactory::getInstance()->create('GET', 'http://foo.com', array(), null, array( + 'auth' => array('michael', 'test') + )); + $this->assertEquals('michael', $request->getUsername()); + $this->assertEquals('test', $request->getPassword()); + } + + public function testCanAddDigestAuth() + { + $request = RequestFactory::getInstance()->create('GET', 'http://foo.com', array(), null, array( + 'auth' => array('michael', 'test', 'digest') + )); + $this->assertEquals(CURLAUTH_DIGEST, $request->getCurlOptions()->get(CURLOPT_HTTPAUTH)); + $this->assertEquals('michael', $request->getUsername()); + $this->assertEquals('test', $request->getPassword()); + } + + public function testCanAddEvents() + { + $foo = null; + $client = new Client(); + $client->addSubscriber(new MockPlugin(array(new Response(200)))); + $request = $client->get($this->getServer()->getUrl(), array(), array( + 'events' => array( + 'request.before_send' => function () use (&$foo) { $foo = true; } + ) + )); + $request->send(); + $this->assertTrue($foo); + } + + public function testCanAddEventsWithPriority() + { + $foo = null; + $client = new Client(); + $client->addSubscriber(new MockPlugin(array(new Response(200)))); + $request = $client->get($this->getServer()->getUrl(), array(), array( + 'events' => array( + 'request.before_send' => array(function () use (&$foo) { $foo = true; }, 100) + ) + )); + $request->send(); + $this->assertTrue($foo); + } + + public function testCanAddPlugins() + { + $mock = new MockPlugin(array(new Response(200))); + $client = new Client(); + $client->addSubscriber($mock); + $request = $client->get('/', array(), array( + 'plugins' => array($mock) + )); + $request->send(); + } + + public function testCanDisableExceptions() + { + $client = new Client(); + $request = $client->get('/', array(), array( + 'plugins' => array(new MockPlugin(array(new Response(500)))), + 'exceptions' => false + )); + $this->assertEquals(500, $request->send()->getStatusCode()); + } + + public function testCanDisableExceptionsWithErrorListener() + { + $client = new Client(); + $client->getEventDispatcher()->addListener('request.error', function () {}); + $request = $client->get('/', array(), array( + 'plugins' => array(new MockPlugin(array(new Response(500)))), + 'exceptions' => false + )); + $this->assertEquals(500, $request->send()->getStatusCode()); + } + + public function testCanChangeSaveToLocation() + { + $r = EntityBody::factory(); + $client = new Client(); + $request = $client->get('/', array(), array( + 'plugins' => array(new MockPlugin(array(new Response(200, array(), 'testing')))), + 'save_to' => $r + )); + $request->send(); + $this->assertEquals('testing', (string) $r); + } + + public function testCanSetProxy() + { + $client = new Client(); + $request = $client->get('/', array(), array('proxy' => '192.168.16.121')); + $this->assertEquals('192.168.16.121', $request->getCurlOptions()->get(CURLOPT_PROXY)); + } + + public function testCanSetHeadersOption() + { + $client = new Client(); + $request = $client->get('/', array(), array('headers' => array('Foo' => 'Bar'))); + $this->assertEquals('Bar', (string) $request->getHeader('Foo')); + } + + public function testCanSetDefaultHeadersOptions() + { + $request = new Request('GET', 'http://www.foo.com', array('Foo' => 'Bar')); + RequestFactory::getInstance()->applyOptions($request, array( + 'headers' => array('Foo' => 'Baz', 'Bam' => 't123') + ), RequestFactory::OPTIONS_AS_DEFAULTS); + $this->assertEquals('Bar', (string) $request->getHeader('Foo')); + $this->assertEquals('t123', (string) $request->getHeader('Bam')); + } + + public function testCanSetBodyOption() + { + $client = new Client(); + $request = $client->put('/', array(), null, array('body' => 'test')); + $this->assertEquals('test', (string) $request->getBody()); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testValidatesBodyOption() + { + $client = new Client(); + $client->get('/', array(), array('body' => 'test')); + } + + public function testCanSetTimeoutOption() + { + $client = new Client(); + $request = $client->get('/', array(), array('timeout' => 1.5)); + $this->assertEquals(1500, $request->getCurlOptions()->get(CURLOPT_TIMEOUT_MS)); + } + + public function testCanSetConnectTimeoutOption() + { + $client = new Client(); + $request = $client->get('/', array(), array('connect_timeout' => 1.5)); + $this->assertEquals(1500, $request->getCurlOptions()->get(CURLOPT_CONNECTTIMEOUT_MS)); + } + + public function testCanSetDebug() + { + $client = new Client(); + $request = $client->get('/', array(), array('debug' => true)); + $this->assertTrue($request->getCurlOptions()->get(CURLOPT_VERBOSE)); + } + + public function testCanSetVerifyToOff() + { + $client = new Client(); + $request = $client->get('/', array(), array('verify' => false)); + $this->assertNull($request->getCurlOptions()->get(CURLOPT_CAINFO)); + $this->assertSame(0, $request->getCurlOptions()->get(CURLOPT_SSL_VERIFYHOST)); + $this->assertFalse($request->getCurlOptions()->get(CURLOPT_SSL_VERIFYPEER)); + } + + public function testCanSetVerifyToOn() + { + $client = new Client(); + $request = $client->get('/', array(), array('verify' => true)); + $this->assertNotNull($request->getCurlOptions()->get(CURLOPT_CAINFO)); + $this->assertSame(2, $request->getCurlOptions()->get(CURLOPT_SSL_VERIFYHOST)); + $this->assertTrue($request->getCurlOptions()->get(CURLOPT_SSL_VERIFYPEER)); + } + + public function testCanSetVerifyToPath() + { + $client = new Client(); + $request = $client->get('/', array(), array('verify' => '/foo.pem')); + $this->assertEquals('/foo.pem', $request->getCurlOptions()->get(CURLOPT_CAINFO)); + $this->assertSame(2, $request->getCurlOptions()->get(CURLOPT_SSL_VERIFYHOST)); + $this->assertTrue($request->getCurlOptions()->get(CURLOPT_SSL_VERIFYPEER)); + } + + public function inputValidation() + { + return array_map(function ($option) { return array($option); }, array( + 'headers', 'query', 'cookies', 'auth', 'events', 'plugins', 'params' + )); + } + + /** + * @dataProvider inputValidation + * @expectedException \Guzzle\Common\Exception\InvalidArgumentException + */ + public function testValidatesInput($option) + { + $client = new Client(); + $client->get('/', array(), array($option => 'foo')); + } + + public function testCanAddRequestParams() + { + $client = new Client(); + $request = $client->put('/', array(), null, array('params' => array('foo' => 'test'))); + $this->assertEquals('test', $request->getParams()->get('foo')); + } + + public function testCanAddSslKey() + { + $client = new Client(); + $request = $client->get('/', array(), array('ssl_key' => '/foo.pem')); + $this->assertEquals('/foo.pem', $request->getCurlOptions()->get(CURLOPT_SSLKEY)); + } + + public function testCanAddSslKeyPassword() + { + $client = new Client(); + $request = $client->get('/', array(), array('ssl_key' => array('/foo.pem', 'bar'))); + $this->assertEquals('/foo.pem', $request->getCurlOptions()->get(CURLOPT_SSLKEY)); + $this->assertEquals('bar', $request->getCurlOptions()->get(CURLOPT_SSLKEYPASSWD)); + } + + public function testCanAddSslCert() + { + $client = new Client(); + $request = $client->get('/', array(), array('cert' => '/foo.pem')); + $this->assertEquals('/foo.pem', $request->getCurlOptions()->get(CURLOPT_SSLCERT)); + } + + public function testCanAddSslCertPassword() + { + $client = new Client(); + $request = $client->get('/', array(), array('cert' => array('/foo.pem', 'bar'))); + $this->assertEquals('/foo.pem', $request->getCurlOptions()->get(CURLOPT_SSLCERT)); + $this->assertEquals('bar', $request->getCurlOptions()->get(CURLOPT_SSLCERTPASSWD)); + } + + public function testCreatesBodyWithoutZeroString() + { + $request = RequestFactory::getInstance()->create('PUT', 'http://test.com', array(), '0'); + $this->assertSame('0', (string) $request->getBody()); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Http/Message/RequestTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Http/Message/RequestTest.php new file mode 100644 index 000000000..5f4d8a789 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Http/Message/RequestTest.php @@ -0,0 +1,625 @@ +client = new Client($this->getServer()->getUrl()); + $this->request = $this->client->get(); + } + + public function tearDown() + { + unset($this->request); + unset($this->client); + } + + public function testConstructorBuildsRequestWithArrayHeaders() + { + // Test passing an array of headers + $request = new Request('GET', 'http://www.guzzle-project.com/', array( + 'foo' => 'bar' + )); + + $this->assertEquals('GET', $request->getMethod()); + $this->assertEquals('http://www.guzzle-project.com/', $request->getUrl()); + $this->assertEquals('bar', $request->getHeader('foo')); + } + + public function testDescribesEvents() + { + $this->assertInternalType('array', Request::getAllEvents()); + } + + public function testConstructorBuildsRequestWithCollectionHeaders() + { + $request = new Request('GET', 'http://www.guzzle-project.com/', new Collection(array( + 'foo' => 'bar' + ))); + $this->assertEquals('bar', $request->getHeader('foo')); + } + + public function testConstructorBuildsRequestWithNoHeaders() + { + $request = new Request('GET', 'http://www.guzzle-project.com/', null); + $this->assertFalse($request->hasHeader('foo')); + } + + public function testConstructorHandlesNonBasicAuth() + { + $request = new Request('GET', 'http://www.guzzle-project.com/', array( + 'Authorization' => 'Foo bar' + )); + $this->assertNull($request->getUserName()); + $this->assertNull($request->getPassword()); + $this->assertEquals('Foo bar', (string) $request->getHeader('Authorization')); + } + + public function testRequestsCanBeConvertedToRawMessageStrings() + { + $auth = base64_encode('michael:123'); + $message = "PUT /path?q=1&v=2 HTTP/1.1\r\n" + . "Host: www.google.com\r\n" + . "Authorization: Basic {$auth}\r\n" + . "Content-Length: 4\r\n\r\nData"; + + $request = RequestFactory::getInstance()->create('PUT', 'http://www.google.com/path?q=1&v=2', array( + 'Authorization' => 'Basic ' . $auth + ), 'Data'); + + $this->assertEquals($message, $request->__toString()); + } + + /** + * Add authorization after the fact and see that it was put in the message + */ + public function testRequestStringsIncludeAuth() + { + $auth = base64_encode('michael:123'); + $this->getServer()->enqueue("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); + $request = RequestFactory::getInstance()->create('PUT', $this->getServer()->getUrl(), null, 'Data') + ->setClient($this->client) + ->setAuth('michael', '123', CURLAUTH_BASIC); + $request->send(); + + $this->assertContains('Authorization: Basic ' . $auth, (string) $request); + } + + public function testGetEventDispatcher() + { + $d = $this->request->getEventDispatcher(); + $this->assertInstanceOf('Symfony\\Component\\EventDispatcher\\EventDispatcherInterface', $d); + $this->assertEquals($d, $this->request->getEventDispatcher()); + } + + public function testRequestsManageClients() + { + $request = new Request('GET', 'http://test.com'); + $this->assertNull($request->getClient()); + $request->setClient($this->client); + $this->assertSame($this->client, $request->getClient()); + } + + /** + * @expectedException \RuntimeException + * @expectedExceptionMessage A client must be set on the request + */ + public function testRequestsRequireClients() + { + $request = new Request('GET', 'http://test.com'); + $request->send(); + } + + public function testSend() + { + $response = new Response(200, array( + 'Content-Length' => 3 + ), 'abc'); + $this->request->setResponse($response, true); + $r = $this->request->send(); + + $this->assertSame($response, $r); + $this->assertInstanceOf('Guzzle\\Http\\Message\\Response', $this->request->getResponse()); + $this->assertSame($r, $this->request->getResponse()); + $this->assertEquals('complete', $this->request->getState()); + } + + public function testGetResponse() + { + $this->assertNull($this->request->getResponse()); + $response = new Response(200, array('Content-Length' => 3), 'abc'); + + $this->request->setResponse($response); + $this->assertEquals($response, $this->request->getResponse()); + + $client = new Client('http://www.google.com'); + $request = $client->get('http://www.google.com/'); + $request->setResponse($response, true); + $request->send(); + $requestResponse = $request->getResponse(); + $this->assertSame($response, $requestResponse); + + // Try again, making sure it's still the same response + $this->assertSame($requestResponse, $request->getResponse()); + + $response = new Response(204); + $request = $client->get(); + $request->setResponse($response, true); + $request->send(); + $requestResponse = $request->getResponse(); + $this->assertSame($response, $requestResponse); + $this->assertInstanceOf('Guzzle\\Http\\EntityBody', $response->getBody()); + } + + public function testRequestThrowsExceptionOnBadResponse() + { + try { + $this->request->setResponse(new Response(404, array('Content-Length' => 3), 'abc'), true); + $this->request->send(); + $this->fail('Expected exception not thrown'); + } catch (BadResponseException $e) { + $this->assertInstanceOf('Guzzle\\Http\\Message\\RequestInterface', $e->getRequest()); + $this->assertInstanceOf('Guzzle\\Http\\Message\\Response', $e->getResponse()); + $this->assertContains('Client error response', $e->getMessage()); + } + } + + public function testManagesQuery() + { + $this->assertInstanceOf('Guzzle\\Http\\QueryString', $this->request->getQuery()); + $this->request->getQuery()->set('test', '123'); + $this->assertEquals('test=123', $this->request->getQuery(true)); + } + + public function testRequestHasMethod() + { + $this->assertEquals('GET', $this->request->getMethod()); + } + + public function testRequestHasScheme() + { + $this->assertEquals('http', $this->request->getScheme()); + $this->assertEquals($this->request, $this->request->setScheme('https')); + $this->assertEquals('https', $this->request->getScheme()); + } + + public function testRequestHasHost() + { + $this->assertEquals('127.0.0.1', $this->request->getHost()); + $this->assertEquals('127.0.0.1:8124', (string) $this->request->getHeader('Host')); + + $this->assertSame($this->request, $this->request->setHost('www2.google.com')); + $this->assertEquals('www2.google.com', $this->request->getHost()); + $this->assertEquals('www2.google.com:8124', (string) $this->request->getHeader('Host')); + + $this->assertSame($this->request, $this->request->setHost('www.test.com:8081')); + $this->assertEquals('www.test.com', $this->request->getHost()); + $this->assertEquals(8081, $this->request->getPort()); + } + + public function testRequestHasProtocol() + { + $this->assertEquals('1.1', $this->request->getProtocolVersion()); + $this->assertEquals($this->request, $this->request->setProtocolVersion('1.1')); + $this->assertEquals('1.1', $this->request->getProtocolVersion()); + $this->assertEquals($this->request, $this->request->setProtocolVersion('1.0')); + $this->assertEquals('1.0', $this->request->getProtocolVersion()); + } + + public function testRequestHasPath() + { + $this->assertEquals('/', $this->request->getPath()); + $this->assertEquals($this->request, $this->request->setPath('/index.html')); + $this->assertEquals('/index.html', $this->request->getPath()); + $this->assertEquals($this->request, $this->request->setPath('index.html')); + $this->assertEquals('/index.html', $this->request->getPath()); + } + + public function testPermitsFalsyComponents() + { + $request = new Request('GET', 'http://0/0?0'); + $this->assertSame('0', $request->getHost()); + $this->assertSame('/0', $request->getPath()); + $this->assertSame('0', $request->getQuery(true)); + + $request = new Request('GET', '0'); + $this->assertEquals('/0', $request->getPath()); + } + + public function testRequestHasPort() + { + $this->assertEquals(8124, $this->request->getPort()); + $this->assertEquals('127.0.0.1:8124', $this->request->getHeader('Host')); + + $this->assertEquals($this->request, $this->request->setPort('8080')); + $this->assertEquals('8080', $this->request->getPort()); + $this->assertEquals('127.0.0.1:8080', $this->request->getHeader('Host')); + + $this->request->setPort(80); + $this->assertEquals('127.0.0.1', $this->request->getHeader('Host')); + } + + public function testRequestHandlesAuthorization() + { + // Uninitialized auth + $this->assertEquals(null, $this->request->getUsername()); + $this->assertEquals(null, $this->request->getPassword()); + + // Set an auth + $this->assertSame($this->request, $this->request->setAuth('michael', '123')); + $this->assertEquals('michael', $this->request->getUsername()); + $this->assertEquals('123', $this->request->getPassword()); + + // Set an auth with blank password + $this->assertSame($this->request, $this->request->setAuth('michael', '')); + $this->assertEquals('michael', $this->request->getUsername()); + $this->assertEquals('', $this->request->getPassword()); + + // Remove the auth + $this->request->setAuth(false); + $this->assertEquals(null, $this->request->getUsername()); + $this->assertEquals(null, $this->request->getPassword()); + + // Make sure that the cURL based auth works too + $request = new Request('GET', $this->getServer()->getUrl()); + $request->setAuth('michael', 'password', CURLAUTH_DIGEST); + $this->assertEquals('michael:password', $request->getCurlOptions()->get(CURLOPT_USERPWD)); + $this->assertEquals(CURLAUTH_DIGEST, $request->getCurlOptions()->get(CURLOPT_HTTPAUTH)); + } + + /** + * @expectedException \Guzzle\Common\Exception\InvalidArgumentException + */ + public function testValidatesAuth() + { + $this->request->setAuth('foo', 'bar', 'bam'); + } + + public function testGetResourceUri() + { + $this->assertEquals('/', $this->request->getResource()); + $this->request->setPath('/index.html'); + $this->assertEquals('/index.html', $this->request->getResource()); + $this->request->getQuery()->add('v', '1'); + $this->assertEquals('/index.html?v=1', $this->request->getResource()); + } + + public function testRequestHasMutableUrl() + { + $url = 'http://www.test.com:8081/path?q=123#fragment'; + $u = Url::factory($url); + $this->assertSame($this->request, $this->request->setUrl($url)); + $this->assertEquals($url, $this->request->getUrl()); + + $this->assertSame($this->request, $this->request->setUrl($u)); + $this->assertEquals($url, $this->request->getUrl()); + } + + public function testRequestHasState() + { + $this->assertEquals(RequestInterface::STATE_NEW, $this->request->getState()); + $this->request->setState(RequestInterface::STATE_TRANSFER); + $this->assertEquals(RequestInterface::STATE_TRANSFER, $this->request->getState()); + } + + public function testSetManualResponse() + { + $response = new Response(200, array( + 'Date' => 'Sat, 16 Oct 2010 17:27:14 GMT', + 'Expires' => '-1', + 'Cache-Control' => 'private, max-age=0', + 'Content-Type' => 'text/html; charset=ISO-8859-1', + ), 'response body'); + + $this->assertSame($this->request, $this->request->setResponse($response), '-> setResponse() must use a fluent interface'); + $this->assertEquals('complete', $this->request->getState(), '-> setResponse() must change the state of the request to complete'); + $this->assertSame($response, $this->request->getResponse(), '-> setResponse() must set the exact same response that was passed in to it'); + } + + public function testRequestCanHaveManuallySetResponseBody() + { + $file = __DIR__ . '/../../TestData/temp.out'; + if (file_exists($file)) { + unlink($file); + } + + $this->getServer()->enqueue("HTTP/1.1 200 OK\r\nContent-Length: 4\r\n\r\ndata"); + $request = RequestFactory::getInstance()->create('GET', $this->getServer()->getUrl()); + $request->setClient($this->client); + $entityBody = EntityBody::factory(fopen($file, 'w+')); + $request->setResponseBody($entityBody); + $response = $request->send(); + $this->assertSame($entityBody, $response->getBody()); + + $this->assertTrue(file_exists($file)); + $this->assertEquals('data', file_get_contents($file)); + unlink($file); + + $this->assertEquals('data', $response->getBody(true)); + } + + public function testHoldsCookies() + { + $this->assertNull($this->request->getCookie('test')); + + // Set a cookie + $this->assertSame($this->request, $this->request->addCookie('test', 'abc')); + $this->assertEquals('abc', $this->request->getCookie('test')); + + // Multiple cookies by setting the Cookie header + $this->request->setHeader('Cookie', '__utma=1.638370270.1344367610.1374365610.1944450276.2; __utmz=1.1346368610.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); hl=de; PHPSESSID=ak93pqashi5uubuoq8fjv60897'); + $this->assertEquals('1.638370270.1344367610.1374365610.1944450276.2', $this->request->getCookie('__utma')); + $this->assertEquals('1.1346368610.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none)', $this->request->getCookie('__utmz')); + $this->assertEquals('de', $this->request->getCookie('hl')); + $this->assertEquals('ak93pqashi5uubuoq8fjv60897', $this->request->getCookie('PHPSESSID')); + + // Unset the cookies by setting the Cookie header to null + $this->request->setHeader('Cookie', null); + $this->assertNull($this->request->getCookie('test')); + $this->request->removeHeader('Cookie'); + + // Set and remove a cookie + $this->assertSame($this->request, $this->request->addCookie('test', 'abc')); + $this->assertEquals('abc', $this->request->getCookie('test')); + $this->assertSame($this->request, $this->request->removeCookie('test')); + $this->assertNull($this->request->getCookie('test')); + + // Remove the cookie header + $this->assertSame($this->request, $this->request->addCookie('test', 'abc')); + $this->request->removeHeader('Cookie'); + $this->assertEquals('', (string) $this->request->getHeader('Cookie')); + + // Remove a cookie value + $this->request->addCookie('foo', 'bar')->addCookie('baz', 'boo'); + $this->request->removeCookie('foo'); + $this->assertEquals(array( + 'baz' => 'boo' + ), $this->request->getCookies()); + + $this->request->addCookie('foo', 'bar'); + $this->assertEquals('baz=boo; foo=bar', (string) $this->request->getHeader('Cookie')); + } + + /** + * @expectedException \Guzzle\Http\Exception\RequestException + * @expectedExceptionMessage Error completing request + */ + public function testRequestThrowsExceptionWhenSetToCompleteWithNoResponse() + { + $this->request->setState(RequestInterface::STATE_COMPLETE); + } + + public function testClonedRequestsUseNewInternalState() + { + $p = new AsyncPlugin(); + $this->request->getEventDispatcher()->addSubscriber($p); + $h = $this->request->getHeader('Host'); + + $r = clone $this->request; + $this->assertEquals(RequestInterface::STATE_NEW, $r->getState()); + $this->assertNotSame($r->getQuery(), $this->request->getQuery()); + $this->assertNotSame($r->getCurlOptions(), $this->request->getCurlOptions()); + $this->assertNotSame($r->getEventDispatcher(), $this->request->getEventDispatcher()); + $this->assertEquals($r->getHeaders(), $this->request->getHeaders()); + $this->assertNotSame($h, $r->getHeader('Host')); + $this->assertNotSame($r->getParams(), $this->request->getParams()); + $this->assertTrue($this->request->getEventDispatcher()->hasListeners('request.sent')); + } + + public function testRecognizesBasicAuthCredentialsInUrls() + { + $this->request->setUrl('http://michael:test@test.com/'); + $this->assertEquals('michael', $this->request->getUsername()); + $this->assertEquals('test', $this->request->getPassword()); + } + + public function testRequestCanBeSentUsingCurl() + { + $this->getServer()->flush(); + $this->getServer()->enqueue(array( + "HTTP/1.1 200 OK\r\nContent-Length: 4\r\nExpires: Thu, 01 Dec 1994 16:00:00 GMT\r\nConnection: close\r\n\r\ndata", + "HTTP/1.1 200 OK\r\nContent-Length: 4\r\nExpires: Thu, 01 Dec 1994 16:00:00 GMT\r\nConnection: close\r\n\r\ndata", + "HTTP/1.1 404 Not Found\r\nContent-Encoding: application/xml\r\nContent-Length: 48\r\n\r\nFile not found" + )); + + $request = RequestFactory::getInstance()->create('GET', $this->getServer()->getUrl()); + $request->setClient($this->client); + $response = $request->send(); + + $this->assertEquals('data', $response->getBody(true)); + $this->assertEquals(200, (int) $response->getStatusCode()); + $this->assertEquals('OK', $response->getReasonPhrase()); + $this->assertEquals(4, $response->getContentLength()); + $this->assertEquals('Thu, 01 Dec 1994 16:00:00 GMT', $response->getExpires()); + + // Test that the same handle can be sent twice without setting state to new + $response2 = $request->send(); + $this->assertNotSame($response, $response2); + + try { + $request = RequestFactory::getInstance()->create('GET', $this->getServer()->getUrl() . 'index.html'); + $request->setClient($this->client); + $response = $request->send(); + $this->fail('Request did not receive a 404 response'); + } catch (BadResponseException $e) { + } + + $requests = $this->getServer()->getReceivedRequests(true); + $messages = $this->getServer()->getReceivedRequests(false); + $port = $this->getServer()->getPort(); + + $userAgent = $this->client->getDefaultUserAgent(); + + $this->assertEquals('127.0.0.1:' . $port, $requests[0]->getHeader('Host')); + $this->assertEquals('127.0.0.1:' . $port, $requests[1]->getHeader('Host')); + $this->assertEquals('127.0.0.1:' . $port, $requests[2]->getHeader('Host')); + + $this->assertEquals('/', $requests[0]->getPath()); + $this->assertEquals('/', $requests[1]->getPath()); + $this->assertEquals('/index.html', $requests[2]->getPath()); + + $parts = explode("\r\n", $messages[0]); + $this->assertEquals('GET / HTTP/1.1', $parts[0]); + + $parts = explode("\r\n", $messages[1]); + $this->assertEquals('GET / HTTP/1.1', $parts[0]); + + $parts = explode("\r\n", $messages[2]); + $this->assertEquals('GET /index.html HTTP/1.1', $parts[0]); + } + + public function testThrowsExceptionsWhenUnsuccessfulResponseIsReceivedByDefault() + { + $this->getServer()->flush(); + $this->getServer()->enqueue("HTTP/1.1 404 Not found\r\nContent-Length: 0\r\n\r\n"); + + try { + $request = $this->client->get('/index.html'); + $response = $request->send(); + $this->fail('Request did not receive a 404 response'); + } catch (BadResponseException $e) { + $this->assertContains('Client error response', $e->getMessage()); + $this->assertContains('[status code] 404', $e->getMessage()); + $this->assertContains('[reason phrase] Not found', $e->getMessage()); + } + } + + public function testCanShortCircuitErrorHandling() + { + $request = $this->request; + $response = new Response(404); + $request->setResponse($response, true); + $out = ''; + $that = $this; + $request->getEventDispatcher()->addListener('request.error', function($event) use (&$out, $that) { + $out .= $event['request'] . "\n" . $event['response'] . "\n"; + $event->stopPropagation(); + }); + $request->send(); + $this->assertContains((string) $request, $out); + $this->assertContains((string) $request->getResponse(), $out); + $this->assertSame($response, $request->getResponse()); + } + + public function testCanOverrideUnsuccessfulResponses() + { + $this->getServer()->flush(); + $this->getServer()->enqueue(array( + "HTTP/1.1 404 NOT FOUND\r\n" . + "Content-Length: 0\r\n" . + "\r\n", + "HTTP/1.1 200 OK\r\n" . + "Content-Length: 0\r\n" . + "\r\n" + )); + + $newResponse = null; + + $request = $this->request; + $request->getEventDispatcher()->addListener('request.error', function($event) use (&$newResponse) { + if ($event['response']->getStatusCode() == 404) { + $newRequest = clone $event['request']; + $newResponse = $newRequest->send(); + // Override the original response and bypass additional response processing + $event['response'] = $newResponse; + // Call $event['request']->setResponse($newResponse); to re-apply events + $event->stopPropagation(); + } + }); + + $request->send(); + + $this->assertEquals(200, $request->getResponse()->getStatusCode()); + $this->assertSame($newResponse, $request->getResponse()); + $this->assertEquals(2, count($this->getServer()->getReceivedRequests())); + } + + public function testCanRetrieveUrlObject() + { + $request = new Request('GET', 'http://www.example.com/foo?abc=d'); + $this->assertInstanceOf('Guzzle\Http\Url', $request->getUrl(true)); + $this->assertEquals('http://www.example.com/foo?abc=d', $request->getUrl()); + $this->assertEquals('http://www.example.com/foo?abc=d', (string) $request->getUrl(true)); + } + + public function testUnresolvedRedirectsReturnResponse() + { + $this->getServer()->flush(); + $this->getServer()->enqueue(array( + "HTTP/1.1 303 SEE OTHER\r\nContent-Length: 0\r\n\r\n", + "HTTP/1.1 301 Foo\r\nLocation: /foo\r\nContent-Length: 0\r\n\r\n" + )); + $request = $this->request; + $this->assertEquals(303, $request->send()->getStatusCode()); + $request->getParams()->set(RedirectPlugin::DISABLE, true); + $this->assertEquals(301, $request->send()->getStatusCode()); + } + + public function testCanSendCustomRequests() + { + $this->getServer()->flush(); + $this->getServer()->enqueue("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); + $request = $this->client->createRequest('PROPFIND', $this->getServer()->getUrl(), array( + 'Content-Type' => 'text/plain' + ), 'foo'); + $response = $request->send(); + $requests = $this->getServer()->getReceivedRequests(true); + $this->assertEquals('PROPFIND', $requests[0]->getMethod()); + $this->assertEquals(3, (string) $requests[0]->getHeader('Content-Length')); + $this->assertEquals('foo', (string) $requests[0]->getBody()); + } + + /** + * @expectedException \PHPUnit_Framework_Error_Warning + */ + public function testEnsuresFileCanBeCreated() + { + $this->getServer()->flush(); + $this->getServer()->enqueue("HTTP/1.1 200 OK\r\nContent-Length: 4\r\n\r\ntest"); + $this->client->get('/')->setResponseBody('/wefwefefefefwewefwe/wefwefwefefwe/wefwefewfw.txt')->send(); + } + + public function testAllowsFilenameForDownloadingContent() + { + $this->getServer()->flush(); + $this->getServer()->enqueue("HTTP/1.1 200 OK\r\nContent-Length: 4\r\n\r\ntest"); + $name = sys_get_temp_dir() . '/foo.txt'; + $this->client->get('/')->setResponseBody($name)->send(); + $this->assertEquals('test', file_get_contents($name)); + unlink($name); + } + + public function testUsesCustomResponseBodyWhenItIsCustom() + { + $en = EntityBody::factory(); + $request = $this->client->get(); + $request->setResponseBody($en); + $request->setResponse(new Response(200, array(), 'foo')); + $this->assertEquals('foo', (string) $en); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Http/Message/ResponseTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Http/Message/ResponseTest.php new file mode 100644 index 000000000..08b4df878 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Http/Message/ResponseTest.php @@ -0,0 +1,677 @@ +response = new Response(200, new Collection(array( + 'Accept-Ranges' => 'bytes', + 'Age' => '12', + 'Allow' => 'GET, HEAD', + 'Cache-Control' => 'no-cache', + 'Content-Encoding' => 'gzip', + 'Content-Language' => 'da', + 'Content-Length' => '348', + 'Content-Location' => '/index.htm', + 'Content-Disposition' => 'attachment; filename=fname.ext', + 'Content-MD5' => 'Q2hlY2sgSW50ZWdyaXR5IQ==', + 'Content-Range' => 'bytes 21010-47021/47022', + 'Content-Type' => 'text/html; charset=utf-8', + 'Date' => 'Tue, 15 Nov 1994 08:12:31 GMT', + 'ETag' => '737060cd8c284d8af7ad3082f209582d', + 'Expires' => 'Thu, 01 Dec 1994 16:00:00 GMT', + 'Last-Modified' => 'Tue, 15 Nov 1994 12:45:26 GMT', + 'Location' => 'http://www.w3.org/pub/WWW/People.html', + 'Pragma' => 'no-cache', + 'Proxy-Authenticate' => 'Basic', + 'Retry-After' => '120', + 'Server' => 'Apache/1.3.27 (Unix) (Red-Hat/Linux)', + 'Set-Cookie' => 'UserID=JohnDoe; Max-Age=3600; Version=1', + 'Trailer' => 'Max-Forwards', + 'Transfer-Encoding' => 'chunked', + 'Vary' => '*', + 'Via' => '1.0 fred, 1.1 nowhere.com (Apache/1.1)', + 'Warning' => '199 Miscellaneous warning', + 'WWW-Authenticate' => 'Basic' + )), 'body'); + } + + public function tearDown() + { + unset($this->response); + } + + public function testConstructor() + { + $params = new Collection(); + $body = EntityBody::factory(''); + $response = new Response(200, $params, $body); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals($body, $response->getBody()); + $this->assertEquals('OK', $response->getReasonPhrase()); + $this->assertEquals("HTTP/1.1 200 OK\r\n\r\n", $response->getRawHeaders()); + + // Make sure Content-Length is set automatically + $response = new Response(200, $params); + $this->assertEquals("HTTP/1.1 200 OK\r\n\r\n", $response->getRawHeaders()); + + // Pass bodies to the response + $response = new Response(200, null, 'data'); + $this->assertInstanceOf('Guzzle\\Http\\EntityBody', $response->getBody()); + $response = new Response(200, null, EntityBody::factory('data')); + $this->assertInstanceOf('Guzzle\\Http\\EntityBody', $response->getBody()); + $this->assertEquals('data', $response->getBody(true)); + $response = new Response(200, null, '0'); + $this->assertSame('0', $response->getBody(true), 'getBody(true) should return "0" if response body is "0".'); + + // Make sure the proper exception is thrown + try { + //$response = new Response(200, null, array('foo' => 'bar')); + //$this->fail('Response did not throw exception when passing invalid body'); + } catch (HttpException $e) { + } + + // Ensure custom codes can be set + $response = new Response(2); + $this->assertEquals(2, $response->getStatusCode()); + $this->assertEquals('', $response->getReasonPhrase()); + + // Make sure the proper exception is thrown when sending invalid headers + try { + $response = new Response(200, 'adidas'); + $this->fail('Response did not throw exception when passing invalid $headers'); + } catch (BadResponseException $e) { + } + } + + public function test__toString() + { + $response = new Response(200); + $this->assertEquals("HTTP/1.1 200 OK\r\n\r\n", (string) $response); + + // Add another header + $response = new Response(200, array( + 'X-Test' => 'Guzzle' + )); + $this->assertEquals("HTTP/1.1 200 OK\r\nX-Test: Guzzle\r\n\r\n", (string) $response); + + $response = new Response(200, array( + 'Content-Length' => 4 + ), 'test'); + $this->assertEquals("HTTP/1.1 200 OK\r\nContent-Length: 4\r\n\r\ntest", (string) $response); + } + + public function testFactory() + { + $response = Response::fromMessage("HTTP/1.1 200 OK\r\nContent-Length: 4\r\n\r\ntest"); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('OK', $response->getReasonPhrase()); + $this->assertEquals(4, (string) $response->getContentLength()); + $this->assertEquals('test', $response->getBody(true)); + + // Make sure that automatic Content-Length works + $response = Response::fromMessage("HTTP/1.1 200 OK\r\nContent-Length: 4\r\n\r\ntest"); + $this->assertEquals(4, (string) $response->getContentLength()); + $this->assertEquals('test', $response->getBody(true)); + } + + public function testFactoryCanCreateHeadResponses() + { + $response = Response::fromMessage("HTTP/1.1 200 OK\r\nContent-Length: 4\r\n\r\n"); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('OK', $response->getReasonPhrase()); + $this->assertEquals(4, (string) $response->getContentLength()); + $this->assertEquals('', $response->getBody(true)); + } + + public function testFactoryRequiresMessage() + { + $this->assertFalse(Response::fromMessage('')); + } + + public function testGetBody() + { + $body = EntityBody::factory(''); + $response = new Response(403, new Collection(), $body); + $this->assertEquals($body, $response->getBody()); + $response->setBody('foo'); + $this->assertEquals('foo', $response->getBody(true)); + } + + public function testManagesStatusCode() + { + $response = new Response(403); + $this->assertEquals(403, $response->getStatusCode()); + } + + public function testGetMessage() + { + $response = new Response(200, new Collection(array( + 'Content-Length' => 4 + )), 'body'); + + $this->assertEquals("HTTP/1.1 200 OK\r\nContent-Length: 4\r\n\r\nbody", $response->getMessage()); + } + + public function testGetRawHeaders() + { + $response = new Response(200, new Collection(array( + 'Keep-Alive' => 155, + 'User-Agent' => 'Guzzle', + 'Content-Length' => 4 + )), 'body'); + + $this->assertEquals("HTTP/1.1 200 OK\r\nKeep-Alive: 155\r\nUser-Agent: Guzzle\r\nContent-Length: 4\r\n\r\n", $response->getRawHeaders()); + } + + public function testHandlesStatusAndStatusCodes() + { + $response = new Response(200, new Collection(), 'body'); + $this->assertEquals('OK', $response->getReasonPhrase()); + + $this->assertSame($response, $response->setStatus(204)); + $this->assertEquals('No Content', $response->getReasonPhrase()); + $this->assertEquals(204, $response->getStatusCode()); + + $this->assertSame($response, $response->setStatus(204, 'Testing!')); + $this->assertEquals('Testing!', $response->getReasonPhrase()); + $this->assertEquals(204, $response->getStatusCode()); + + $response->setStatus(2000); + $this->assertEquals(2000, $response->getStatusCode()); + $this->assertEquals('', $response->getReasonPhrase()); + + $response->setStatus(200, 'Foo'); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('Foo', $response->getReasonPhrase()); + } + + public function testIsClientError() + { + $response = new Response(403); + $this->assertTrue($response->isClientError()); + $response = new Response(200); + $this->assertFalse($response->isClientError()); + } + + public function testIsError() + { + $response = new Response(403); + $this->assertTrue($response->isError()); + $response = new Response(200); + $this->assertFalse($response->isError()); + $response = new Response(500); + $this->assertTrue($response->isError()); + } + + public function testIsInformational() + { + $response = new Response(100); + $this->assertTrue($response->isInformational()); + $response = new Response(200); + $this->assertFalse($response->isInformational()); + } + + public function testIsRedirect() + { + $response = new Response(301); + $this->assertTrue($response->isRedirect()); + $response = new Response(200); + $this->assertFalse($response->isRedirect()); + } + + public function testIsServerError() + { + $response = new Response(500); + $this->assertTrue($response->isServerError()); + $response = new Response(400); + $this->assertFalse($response->isServerError()); + } + + public function testIsSuccessful() + { + $response = new Response(200); + $this->assertTrue($response->isSuccessful()); + $response = new Response(403); + $this->assertFalse($response->isSuccessful()); + } + + public function testGetAcceptRanges() + { + $this->assertEquals('bytes', $this->response->getAcceptRanges()); + } + + public function testCalculatesAge() + { + $this->assertEquals(12, $this->response->calculateAge()); + + $this->response->removeHeader('Age'); + $this->response->removeHeader('Date'); + $this->assertNull($this->response->calculateAge()); + + $this->response->setHeader('Date', gmdate(ClientInterface::HTTP_DATE, strtotime('-1 minute'))); + // If the test runs slowly, still pass with a +5 second allowance + $this->assertTrue($this->response->getAge() - 60 <= 5); + } + + public function testGetAllow() + { + $this->assertEquals('GET, HEAD', $this->response->getAllow()); + } + + public function testGetCacheControl() + { + $this->assertEquals('no-cache', $this->response->getCacheControl()); + } + + public function testGetContentEncoding() + { + $this->assertEquals('gzip', $this->response->getContentEncoding()); + } + + public function testGetContentLanguage() + { + $this->assertEquals('da', $this->response->getContentLanguage()); + } + + public function testGetContentLength() + { + $this->assertEquals('348', $this->response->getContentLength()); + } + + public function testGetContentLocation() + { + $this->assertEquals('/index.htm', $this->response->getContentLocation()); + } + + public function testGetContentDisposition() + { + $this->assertEquals('attachment; filename=fname.ext', $this->response->getContentDisposition()); + } + + public function testGetContentMd5() + { + $this->assertEquals('Q2hlY2sgSW50ZWdyaXR5IQ==', $this->response->getContentMd5()); + } + + public function testGetContentRange() + { + $this->assertEquals('bytes 21010-47021/47022', $this->response->getContentRange()); + } + + public function testGetContentType() + { + $this->assertEquals('text/html; charset=utf-8', $this->response->getContentType()); + } + + public function testGetDate() + { + $this->assertEquals('Tue, 15 Nov 1994 08:12:31 GMT', $this->response->getDate()); + } + + public function testGetEtag() + { + $this->assertEquals('737060cd8c284d8af7ad3082f209582d', $this->response->getEtag()); + } + + public function testGetExpires() + { + $this->assertEquals('Thu, 01 Dec 1994 16:00:00 GMT', $this->response->getExpires()); + } + + public function testGetLastModified() + { + $this->assertEquals('Tue, 15 Nov 1994 12:45:26 GMT', $this->response->getLastModified()); + } + + public function testGetLocation() + { + $this->assertEquals('http://www.w3.org/pub/WWW/People.html', $this->response->getLocation()); + } + + public function testGetPragma() + { + $this->assertEquals('no-cache', $this->response->getPragma()); + } + + public function testGetProxyAuthenticate() + { + $this->assertEquals('Basic', $this->response->getProxyAuthenticate()); + } + + public function testGetServer() + { + $this->assertEquals('Apache/1.3.27 (Unix) (Red-Hat/Linux)', $this->response->getServer()); + } + + public function testGetSetCookie() + { + $this->assertEquals('UserID=JohnDoe; Max-Age=3600; Version=1', $this->response->getSetCookie()); + } + + public function testGetMultipleSetCookie() + { + $this->response->addHeader('Set-Cookie', 'UserID=Mike; Max-Age=200'); + $this->assertEquals(array( + 'UserID=JohnDoe; Max-Age=3600; Version=1', + 'UserID=Mike; Max-Age=200', + ), $this->response->getHeader('Set-Cookie')->toArray()); + } + + public function testGetSetCookieNormalizesHeaders() + { + $this->response->addHeaders(array( + 'Set-Cooke' => 'boo', + 'set-cookie' => 'foo' + )); + + $this->assertEquals(array( + 'UserID=JohnDoe; Max-Age=3600; Version=1', + 'foo' + ), $this->response->getHeader('Set-Cookie')->toArray()); + + $this->response->addHeaders(array( + 'set-cookie' => 'fubu' + )); + $this->assertEquals( + array('UserID=JohnDoe; Max-Age=3600; Version=1', 'foo', 'fubu'), + $this->response->getHeader('Set-Cookie')->toArray() + ); + } + + public function testGetTrailer() + { + $this->assertEquals('Max-Forwards', $this->response->getTrailer()); + } + + public function testGetTransferEncoding() + { + $this->assertEquals('chunked', $this->response->getTransferEncoding()); + } + + public function testGetVary() + { + $this->assertEquals('*', $this->response->getVary()); + } + + public function testReturnsViaHeader() + { + $this->assertEquals('1.0 fred, 1.1 nowhere.com (Apache/1.1)', $this->response->getVia()); + } + public function testGetWarning() + { + $this->assertEquals('199 Miscellaneous warning', $this->response->getWarning()); + } + + public function testReturnsWwwAuthenticateHeader() + { + $this->assertEquals('Basic', $this->response->getWwwAuthenticate()); + } + + public function testReturnsConnectionHeader() + { + $this->assertEquals(null, $this->response->getConnection()); + $this->response->setHeader('Connection', 'close'); + $this->assertEquals('close', $this->response->getConnection()); + } + + public function testReturnsHeaders() + { + $this->assertEquals('Basic', $this->response->getHeader('WWW-Authenticate', null, true)); + $this->assertEquals('chunked', $this->response->getHeader('Transfer-Encoding', null, false)); + } + + public function testHasTransferInfo() + { + $stats = array ( + 'url' => 'http://www.google.com/', + 'content_type' => 'text/html; charset=ISO-8859-1', + 'http_code' => 200, + 'header_size' => 606, + 'request_size' => 53, + 'filetime' => -1, + 'ssl_verify_result' => 0, + 'redirect_count' => 0, + 'total_time' => 0.093284, + 'namelookup_time' => 0.001349, + 'connect_time' => 0.01635, + 'pretransfer_time' => 0.016358, + 'size_upload' => 0, + 'size_download' => 10330, + 'speed_download' => 110737, + 'speed_upload' => 0, + 'download_content_length' => -1, + 'upload_content_length' => 0, + 'starttransfer_time' => 0.07066, + 'redirect_time' => 0, + ); + + // Uninitialized state + $this->assertNull($this->response->getInfo('url')); + $this->assertEquals(array(), $this->response->getInfo()); + + // Set the stats + $this->response->setInfo($stats); + $this->assertEquals($stats, $this->response->getInfo()); + $this->assertEquals(606, $this->response->getInfo('header_size')); + $this->assertNull($this->response->getInfo('does_not_exist')); + } + + /** + * @return Response + */ + private function getResponse($code, array $headers = null, EntityBody $body = null) + { + return new Response($code, $headers, $body); + } + + public function testDeterminesIfItCanBeCached() + { + $this->assertTrue($this->getResponse(200)->canCache()); + $this->assertTrue($this->getResponse(410)->canCache()); + $this->assertFalse($this->getResponse(404)->canCache()); + $this->assertTrue($this->getResponse(200, array( + 'Cache-Control' => 'public' + ))->canCache()); + + // This has the no-store directive + $this->assertFalse($this->getResponse(200, array( + 'Cache-Control' => 'private, no-store' + ))->canCache()); + + // The body cannot be read, so it cannot be cached + $tmp = tempnam('/tmp', 'not-readable'); + $resource = fopen($tmp, 'w'); + $this->assertFalse($this->getResponse(200, array( + 'Transfer-Encoding' => 'chunked' + ), EntityBody::factory($resource, 10))->canCache()); + unlink($tmp); + + // The body is 0 length, cannot be read, so it can be cached + $tmp = tempnam('/tmp', 'not-readable'); + $resource = fopen($tmp, 'w'); + $this->assertTrue($this->getResponse(200, array(array( + 'Content-Length' => 0 + )), EntityBody::factory($resource, 0))->canCache()); + unlink($tmp); + } + + public function testDeterminesResponseMaxAge() + { + $this->assertEquals(null, $this->getResponse(200)->getMaxAge()); + + // Uses the response's s-maxage + $this->assertEquals(140, $this->getResponse(200, array( + 'Cache-Control' => 's-maxage=140' + ))->getMaxAge()); + + // Uses the response's max-age + $this->assertEquals(120, $this->getResponse(200, array( + 'Cache-Control' => 'max-age=120' + ))->getMaxAge()); + + // Uses the response's max-age + $this->assertEquals(120, $this->getResponse(200, array( + 'Cache-Control' => 'max-age=120', + 'Expires' => gmdate(ClientInterface::HTTP_DATE, strtotime('+1 day')) + ))->getMaxAge()); + + // Uses the Expires date + $this->assertGreaterThanOrEqual(82400, $this->getResponse(200, array( + 'Expires' => gmdate(ClientInterface::HTTP_DATE, strtotime('+1 day')) + ))->getMaxAge()); + + // Uses the Expires date + $this->assertGreaterThanOrEqual(82400, $this->getResponse(200, array( + 'Expires' => gmdate(ClientInterface::HTTP_DATE, strtotime('+1 day')) + ))->getMaxAge()); + } + + public function testDeterminesIfItCanValidate() + { + $response = new Response(200); + $this->assertFalse($response->canValidate()); + $response->setHeader('ETag', '123'); + $this->assertTrue($response->canValidate()); + $response->removeHeader('ETag'); + $this->assertFalse($response->canValidate()); + $response->setHeader('Last-Modified', '123'); + $this->assertTrue($response->canValidate()); + } + + public function testCalculatesFreshness() + { + $response = new Response(200); + $this->assertNull($response->isFresh()); + $this->assertNull($response->getFreshness()); + + $response->setHeader('Cache-Control', 'max-age=120'); + $response->setHeader('Age', 100); + $this->assertEquals(20, $response->getFreshness()); + $this->assertTrue($response->isFresh()); + + $response->setHeader('Age', 120); + $this->assertEquals(0, $response->getFreshness()); + $this->assertTrue($response->isFresh()); + + $response->setHeader('Age', 150); + $this->assertEquals(-30, $response->getFreshness()); + $this->assertFalse($response->isFresh()); + } + + public function testHandlesProtocols() + { + $this->assertSame($this->response, $this->response->setProtocol('HTTP', '1.0')); + $this->assertEquals('HTTP', $this->response->getProtocol()); + $this->assertEquals('1.0', $this->response->getProtocolVersion()); + } + + public function testComparesContentType() + { + $response = new Response(200, array( + 'Content-Type' => 'text/html; charset=ISO-8859-4' + )); + + $this->assertTrue($response->isContentType('text/html')); + $this->assertTrue($response->isContentType('TExT/html')); + $this->assertTrue($response->isContentType('charset=ISO-8859-4')); + $this->assertFalse($response->isContentType('application/xml')); + } + + public function testResponseDeterminesIfMethodIsAllowedBaseOnAllowHeader() + { + $response = new Response(200, array( + 'Allow' => 'OPTIONS, POST, deletE,GET' + )); + + $this->assertTrue($response->isMethodAllowed('get')); + $this->assertTrue($response->isMethodAllowed('GET')); + $this->assertTrue($response->isMethodAllowed('options')); + $this->assertTrue($response->isMethodAllowed('post')); + $this->assertTrue($response->isMethodAllowed('Delete')); + $this->assertFalse($response->isMethodAllowed('put')); + $this->assertFalse($response->isMethodAllowed('PUT')); + + $response = new Response(200); + $this->assertFalse($response->isMethodAllowed('get')); + } + + public function testParsesJsonResponses() + { + $response = new Response(200, array(), '{"foo": "bar"}'); + $this->assertEquals(array('foo' => 'bar'), $response->json()); + // Return array when null is a service response + $response = new Response(200); + $this->assertEquals(array(), $response->json()); + } + + /** + * @expectedException \Guzzle\Common\Exception\RuntimeException + * @expectedExceptionMessage Unable to parse response body into JSON: 4 + */ + public function testThrowsExceptionWhenFailsToParseJsonResponse() + { + $response = new Response(200, array(), '{"foo": "'); + $response->json(); + } + + public function testParsesXmlResponses() + { + $response = new Response(200, array(), 'bar'); + $this->assertEquals('bar', (string) $response->xml()->foo); + // Always return a SimpleXMLElement from the xml method + $response = new Response(200); + $this->assertEmpty((string) $response->xml()->foo); + } + + /** + * @expectedException \Guzzle\Common\Exception\RuntimeException + * @expectedExceptionMessage Unable to parse response body into XML: String could not be parsed as XML + */ + public function testThrowsExceptionWhenFailsToParseXmlResponse() + { + $response = new Response(200, array(), 'xml(); + } + + public function testResponseIsSerializable() + { + $response = new Response(200, array('Foo' => 'bar'), 'test'); + $r = unserialize(serialize($response)); + $this->assertEquals(200, $r->getStatusCode()); + $this->assertEquals('bar', (string) $r->getHeader('Foo')); + $this->assertEquals('test', (string) $r->getBody()); + } + + public function testPreventsComplexExternalEntities() + { + $xml = ']>&test;'; + $response = new Response(200, array(), $xml); + + $oldCwd = getcwd(); + chdir(__DIR__); + try { + $xml = $response->xml(); + chdir($oldCwd); + $this->markTestIncomplete('Did not throw the expected exception! XML resolved as: ' . $xml->asXML()); + } catch (\Exception $e) { + chdir($oldCwd); + } + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Http/MimetypesTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Http/MimetypesTest.php new file mode 100644 index 000000000..722845340 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Http/MimetypesTest.php @@ -0,0 +1,31 @@ +assertEquals('text/x-php', Mimetypes::getInstance()->fromExtension('php')); + } + + public function testGetsFromFilename() + { + $this->assertEquals('text/x-php', Mimetypes::getInstance()->fromFilename(__FILE__)); + } + + public function testGetsFromCaseInsensitiveFilename() + { + $this->assertEquals('text/x-php', Mimetypes::getInstance()->fromFilename(strtoupper(__FILE__))); + } + + public function testReturnsNullWhenNoMatchFound() + { + $this->assertNull(Mimetypes::getInstance()->fromExtension('foobar')); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Http/QueryAggregator/CommaAggregatorTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Http/QueryAggregator/CommaAggregatorTest.php new file mode 100644 index 000000000..549d3edd9 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Http/QueryAggregator/CommaAggregatorTest.php @@ -0,0 +1,30 @@ +aggregate($key, $value, $query); + $this->assertEquals(array('test%20123' => 'foo%20123,baz,bar'), $result); + } + + public function testEncodes() + { + $query = new QueryString(); + $query->useUrlEncoding(false); + $a = new Ag(); + $key = 'test 123'; + $value = array('foo 123', 'baz', 'bar'); + $result = $a->aggregate($key, $value, $query); + $this->assertEquals(array('test 123' => 'foo 123,baz,bar'), $result); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Http/QueryAggregator/DuplicateAggregatorTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Http/QueryAggregator/DuplicateAggregatorTest.php new file mode 100644 index 000000000..6a4d9d95b --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Http/QueryAggregator/DuplicateAggregatorTest.php @@ -0,0 +1,30 @@ +aggregate($key, $value, $query); + $this->assertEquals(array('facet%201' => array('size%20a', 'width%20b')), $result); + } + + public function testEncodes() + { + $query = new QueryString(); + $query->useUrlEncoding(false); + $a = new Ag(); + $key = 'facet 1'; + $value = array('size a', 'width b'); + $result = $a->aggregate($key, $value, $query); + $this->assertEquals(array('facet 1' => array('size a', 'width b')), $result); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Http/QueryAggregator/PhpAggregatorTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Http/QueryAggregator/PhpAggregatorTest.php new file mode 100644 index 000000000..1e7f0c27a --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Http/QueryAggregator/PhpAggregatorTest.php @@ -0,0 +1,32 @@ +useUrlEncoding(false); + $a = new Ag(); + $key = 't'; + $value = array( + 'v1' => 'a', + 'v2' => 'b', + 'v3' => array( + 'v4' => 'c', + 'v5' => 'd', + ) + ); + $result = $a->aggregate($key, $value, $query); + $this->assertEquals(array( + 't[v1]' => 'a', + 't[v2]' => 'b', + 't[v3][v4]' => 'c', + 't[v3][v5]' => 'd', + ), $result); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Http/QueryStringTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Http/QueryStringTest.php new file mode 100644 index 000000000..0513f6224 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Http/QueryStringTest.php @@ -0,0 +1,232 @@ +q = new QueryString(); + } + + public function testGetFieldSeparator() + { + $this->assertEquals('&', $this->q->getFieldSeparator()); + } + + public function testGetValueSeparator() + { + $this->assertEquals('=', $this->q->getValueSeparator()); + } + + public function testIsUrlEncoding() + { + $this->assertEquals('RFC 3986', $this->q->getUrlEncoding()); + $this->assertTrue($this->q->isUrlEncoding()); + $this->assertEquals('foo%20bar', $this->q->encodeValue('foo bar')); + + $this->q->useUrlEncoding(QueryString::FORM_URLENCODED); + $this->assertTrue($this->q->isUrlEncoding()); + $this->assertEquals(QueryString::FORM_URLENCODED, $this->q->getUrlEncoding()); + $this->assertEquals('foo+bar', $this->q->encodeValue('foo bar')); + + $this->assertSame($this->q, $this->q->useUrlEncoding(false)); + $this->assertFalse($this->q->isUrlEncoding()); + $this->assertFalse($this->q->isUrlEncoding()); + } + + public function testSetFieldSeparator() + { + $this->assertEquals($this->q, $this->q->setFieldSeparator('/')); + $this->assertEquals('/', $this->q->getFieldSeparator()); + } + + public function testSetValueSeparator() + { + $this->assertEquals($this->q, $this->q->setValueSeparator('/')); + $this->assertEquals('/', $this->q->getValueSeparator()); + } + + public function testUrlEncode() + { + $params = array( + 'test' => 'value', + 'test 2' => 'this is a test?', + 'test3' => array('v1', 'v2', 'v3'), + 'ሴ' => 'bar' + ); + $encoded = array( + 'test' => 'value', + 'test%202' => rawurlencode('this is a test?'), + 'test3%5B0%5D' => 'v1', + 'test3%5B1%5D' => 'v2', + 'test3%5B2%5D' => 'v3', + '%E1%88%B4' => 'bar' + ); + $this->q->replace($params); + $this->assertEquals($encoded, $this->q->urlEncode()); + + // Disable encoding + $testData = array('test 2' => 'this is a test'); + $this->q->replace($testData); + $this->q->useUrlEncoding(false); + $this->assertEquals($testData, $this->q->urlEncode()); + } + + public function testToString() + { + // Check with no parameters + $this->assertEquals('', $this->q->__toString()); + + $params = array( + 'test' => 'value', + 'test 2' => 'this is a test?', + 'test3' => array('v1', 'v2', 'v3'), + 'test4' => null, + ); + $this->q->replace($params); + $this->assertEquals('test=value&test%202=this%20is%20a%20test%3F&test3%5B0%5D=v1&test3%5B1%5D=v2&test3%5B2%5D=v3&test4=', $this->q->__toString()); + $this->q->useUrlEncoding(false); + $this->assertEquals('test=value&test 2=this is a test?&test3[0]=v1&test3[1]=v2&test3[2]=v3&test4=', $this->q->__toString()); + + // Use an alternative aggregator + $this->q->setAggregator(new CommaAggregator()); + $this->assertEquals('test=value&test 2=this is a test?&test3=v1,v2,v3&test4=', $this->q->__toString()); + } + + public function testAllowsMultipleValuesPerKey() + { + $q = new QueryString(); + $q->add('facet', 'size'); + $q->add('facet', 'width'); + $q->add('facet.field', 'foo'); + // Use the duplicate aggregator + $q->setAggregator(new DuplicateAggregator()); + $this->assertEquals('facet=size&facet=width&facet.field=foo', $q->__toString()); + } + + public function testAllowsNestedQueryData() + { + $this->q->replace(array( + 'test' => 'value', + 't' => array( + 'v1' => 'a', + 'v2' => 'b', + 'v3' => array( + 'v4' => 'c', + 'v5' => 'd', + ) + ) + )); + + $this->q->useUrlEncoding(false); + $this->assertEquals('test=value&t[v1]=a&t[v2]=b&t[v3][v4]=c&t[v3][v5]=d', $this->q->__toString()); + } + + public function parseQueryProvider() + { + return array( + // Ensure that multiple query string values are allowed per value + array('q=a&q=b', array('q' => array('a', 'b'))), + // Ensure that PHP array style query string values are parsed + array('q[]=a&q[]=b', array('q' => array('a', 'b'))), + // Ensure that a single PHP array style query string value is parsed into an array + array('q[]=a', array('q' => array('a'))), + // Ensure that decimals are allowed in query strings + array('q.a=a&q.b=b', array( + 'q.a' => 'a', + 'q.b' => 'b' + )), + // Ensure that query string values are percent decoded + array('q%20a=a%20b', array('q a' => 'a b')), + // Ensure null values can be added + array('q&a', array('q' => QueryString::BLANK, 'a' => QueryString::BLANK)), + ); + } + + /** + * @dataProvider parseQueryProvider + */ + public function testParsesQueryStrings($query, $data) + { + $query = QueryString::fromString($query); + $this->assertEquals($data, $query->getAll()); + } + + public function testProperlyDealsWithDuplicateQueryStringValues() + { + $query = QueryString::fromString('foo=a&foo=b&?µ=c'); + $this->assertEquals(array('a', 'b'), $query->get('foo')); + $this->assertEquals('c', $query->get('?µ')); + } + + public function testAllowsBlankQueryStringValues() + { + $query = QueryString::fromString('foo'); + $this->assertEquals('foo', (string) $query); + $query->set('foo', QueryString::BLANK); + $this->assertEquals('foo', (string) $query); + } + + public function testAllowsFalsyQueryStringValues() + { + $query = QueryString::fromString('0'); + $this->assertEquals('0', (string) $query); + $query->set('0', QueryString::BLANK); + $this->assertSame('0', (string) $query); + } + + public function testFromStringIgnoresQuestionMark() + { + $query = QueryString::fromString('foo=baz&bar=boo'); + $this->assertEquals('foo=baz&bar=boo', (string) $query); + } + + public function testConvertsPlusSymbolsToSpaces() + { + $query = QueryString::fromString('var=foo+bar'); + $this->assertEquals('foo bar', $query->get('var')); + } + + public function testFromStringDoesntMangleZeroes() + { + $query = QueryString::fromString('var=0'); + $this->assertSame('0', $query->get('var')); + } + + public function testAllowsZeroValues() + { + $query = new QueryString(array( + 'foo' => 0, + 'baz' => '0', + 'bar' => null, + 'boo' => false + )); + $this->assertEquals('foo=0&baz=0&bar=&boo=', (string) $query); + } + + public function testFromStringDoesntStripTrailingEquals() + { + $query = QueryString::fromString('data=mF0b3IiLCJUZWFtIERldiJdfX0='); + $this->assertEquals('mF0b3IiLCJUZWFtIERldiJdfX0=', $query->get('data')); + } + + public function testGuessesIfDuplicateAggregatorShouldBeUsed() + { + $query = QueryString::fromString('test=a&test=b'); + $this->assertEquals('test=a&test=b', (string) $query); + } + + public function testGuessesIfDuplicateAggregatorShouldBeUsedAndChecksForPhpStyle() + { + $query = QueryString::fromString('test[]=a&test[]=b'); + $this->assertEquals('test%5B0%5D=a&test%5B1%5D=b', (string) $query); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Http/ReadLimitEntityBodyTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Http/ReadLimitEntityBodyTest.php new file mode 100644 index 000000000..6bb3fed18 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Http/ReadLimitEntityBodyTest.php @@ -0,0 +1,81 @@ +decorated = EntityBody::factory(fopen(__FILE__, 'r')); + $this->body = new ReadLimitEntityBody($this->decorated, 10, 3); + } + + public function testReturnsSubsetWhenCastToString() + { + $body = EntityBody::factory('foo_baz_bar'); + $limited = new ReadLimitEntityBody($body, 3, 4); + $this->assertEquals('baz', (string) $limited); + } + + public function testReturnsSubsetOfEmptyBodyWhenCastToString() + { + $body = EntityBody::factory(''); + $limited = new ReadLimitEntityBody($body, 0, 10); + $this->assertEquals('', (string) $limited); + } + + public function testSeeksWhenConstructed() + { + $this->assertEquals(3, $this->body->ftell()); + } + + public function testAllowsBoundedSeek() + { + $this->body->seek(100); + $this->assertEquals(13, $this->body->ftell()); + $this->body->seek(0); + $this->assertEquals(3, $this->body->ftell()); + $this->assertEquals(false, $this->body->seek(1000, SEEK_END)); + } + + public function testReadsOnlySubsetOfData() + { + $data = $this->body->read(100); + $this->assertEquals(10, strlen($data)); + $this->assertFalse($this->body->read(1000)); + + $this->body->setOffset(10); + $newData = $this->body->read(100); + $this->assertEquals(10, strlen($newData)); + $this->assertNotSame($data, $newData); + } + + public function testClaimsConsumedWhenReadLimitIsReached() + { + $this->assertFalse($this->body->isConsumed()); + $this->body->read(1000); + $this->assertTrue($this->body->isConsumed()); + } + + public function testContentLengthIsBounded() + { + $this->assertEquals(10, $this->body->getContentLength()); + } + + public function testContentMd5IsBasedOnSubsection() + { + $this->assertNotSame($this->body->getContentMd5(), $this->decorated->getContentMd5()); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Http/RedirectPluginTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Http/RedirectPluginTest.php new file mode 100644 index 000000000..886236ddd --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Http/RedirectPluginTest.php @@ -0,0 +1,277 @@ +getServer()->flush(); + $this->getServer()->enqueue(array( + "HTTP/1.1 301 Moved Permanently\r\nLocation: /redirect1\r\nContent-Length: 0\r\n\r\n", + "HTTP/1.1 301 Moved Permanently\r\nLocation: /redirect2\r\nContent-Length: 0\r\n\r\n", + "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n", + )); + + // Create a client that uses the default redirect behavior + $client = new Client($this->getServer()->getUrl()); + $history = new HistoryPlugin(); + $client->addSubscriber($history); + + $request = $client->get('/foo'); + $response = $request->send(); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertContains('/redirect2', $response->getEffectiveUrl()); + + // Ensure that two requests were sent + $requests = $this->getServer()->getReceivedRequests(true); + $this->assertEquals('/foo', $requests[0]->getResource()); + $this->assertEquals('GET', $requests[0]->getMethod()); + $this->assertEquals('/redirect1', $requests[1]->getResource()); + $this->assertEquals('GET', $requests[1]->getMethod()); + $this->assertEquals('/redirect2', $requests[2]->getResource()); + $this->assertEquals('GET', $requests[2]->getMethod()); + + // Ensure that the redirect count was incremented + $this->assertEquals(2, $request->getParams()->get(RedirectPlugin::REDIRECT_COUNT)); + $this->assertCount(3, $history); + $requestHistory = $history->getAll(); + + $this->assertEquals(301, $requestHistory[0]['response']->getStatusCode()); + $this->assertEquals('/redirect1', (string) $requestHistory[0]['response']->getHeader('Location')); + $this->assertEquals(301, $requestHistory[1]['response']->getStatusCode()); + $this->assertEquals('/redirect2', (string) $requestHistory[1]['response']->getHeader('Location')); + $this->assertEquals(200, $requestHistory[2]['response']->getStatusCode()); + } + + public function testCanLimitNumberOfRedirects() + { + // Flush the server and queue up a redirect followed by a successful response + $this->getServer()->flush(); + $this->getServer()->enqueue(array( + "HTTP/1.1 301 Moved Permanently\r\nLocation: /redirect1\r\nContent-Length: 0\r\n\r\n", + "HTTP/1.1 301 Moved Permanently\r\nLocation: /redirect2\r\nContent-Length: 0\r\n\r\n", + "HTTP/1.1 301 Moved Permanently\r\nLocation: /redirect3\r\nContent-Length: 0\r\n\r\n", + "HTTP/1.1 301 Moved Permanently\r\nLocation: /redirect4\r\nContent-Length: 0\r\n\r\n", + "HTTP/1.1 301 Moved Permanently\r\nLocation: /redirect5\r\nContent-Length: 0\r\n\r\n", + "HTTP/1.1 301 Moved Permanently\r\nLocation: /redirect6\r\nContent-Length: 0\r\n\r\n" + )); + + try { + $client = new Client($this->getServer()->getUrl()); + $client->get('/foo')->send(); + $this->fail('Did not throw expected exception'); + } catch (TooManyRedirectsException $e) { + $this->assertContains( + "5 redirects were issued for this request:\nGET /foo HTTP/1.1\r\n", + $e->getMessage() + ); + } + } + + public function testDefaultBehaviorIsToRedirectWithGetForEntityEnclosingRequests() + { + $this->getServer()->flush(); + $this->getServer()->enqueue(array( + "HTTP/1.1 301 Moved Permanently\r\nLocation: /redirect\r\nContent-Length: 0\r\n\r\n", + "HTTP/1.1 301 Moved Permanently\r\nLocation: /redirect\r\nContent-Length: 0\r\n\r\n", + "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n", + )); + + $client = new Client($this->getServer()->getUrl()); + $client->post('/foo', array('X-Baz' => 'bar'), 'testing')->send(); + + $requests = $this->getServer()->getReceivedRequests(true); + $this->assertEquals('POST', $requests[0]->getMethod()); + $this->assertEquals('GET', $requests[1]->getMethod()); + $this->assertEquals('bar', (string) $requests[1]->getHeader('X-Baz')); + $this->assertEquals('GET', $requests[2]->getMethod()); + } + + public function testCanRedirectWithStrictRfcCompliance() + { + $this->getServer()->flush(); + $this->getServer()->enqueue(array( + "HTTP/1.1 301 Moved Permanently\r\nLocation: /redirect\r\nContent-Length: 0\r\n\r\n", + "HTTP/1.1 301 Moved Permanently\r\nLocation: /redirect\r\nContent-Length: 0\r\n\r\n", + "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n", + )); + + $client = new Client($this->getServer()->getUrl()); + $request = $client->post('/foo', array('X-Baz' => 'bar'), 'testing'); + $request->getParams()->set(RedirectPlugin::STRICT_REDIRECTS, true); + $request->send(); + + $requests = $this->getServer()->getReceivedRequests(true); + $this->assertEquals('POST', $requests[0]->getMethod()); + $this->assertEquals('POST', $requests[1]->getMethod()); + $this->assertEquals('bar', (string) $requests[1]->getHeader('X-Baz')); + $this->assertEquals('POST', $requests[2]->getMethod()); + } + + public function testRedirect303WithGet() + { + $this->getServer()->flush(); + $this->getServer()->enqueue(array( + "HTTP/1.1 303 Moved Permanently\r\nLocation: /redirect\r\nContent-Length: 0\r\n\r\n", + "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n", + )); + + $client = new Client($this->getServer()->getUrl()); + $request = $client->post('/foo'); + $request->send(); + + $requests = $this->getServer()->getReceivedRequests(true); + $this->assertEquals('POST', $requests[0]->getMethod()); + $this->assertEquals('GET', $requests[1]->getMethod()); + } + + public function testRedirect303WithGetWithStrictRfcCompliance() + { + $this->getServer()->flush(); + $this->getServer()->enqueue(array( + "HTTP/1.1 303 Moved Permanently\r\nLocation: /redirect\r\nContent-Length: 0\r\n\r\n", + "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n", + )); + + $client = new Client($this->getServer()->getUrl()); + $request = $client->post('/foo'); + $request->getParams()->set(RedirectPlugin::STRICT_REDIRECTS, true); + $request->send(); + + $requests = $this->getServer()->getReceivedRequests(true); + $this->assertEquals('POST', $requests[0]->getMethod()); + $this->assertEquals('GET', $requests[1]->getMethod()); + } + + public function testRewindsStreamWhenRedirectingIfNeeded() + { + $this->getServer()->flush(); + $this->getServer()->enqueue(array( + "HTTP/1.1 301 Moved Permanently\r\nLocation: /redirect\r\nContent-Length: 0\r\n\r\n", + "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n", + )); + + $client = new Client($this->getServer()->getUrl()); + $request = $client->put(); + $request->configureRedirects(true); + $body = EntityBody::factory('foo'); + $body->read(1); + $request->setBody($body); + $request->send(); + $requests = $this->getServer()->getReceivedRequests(true); + $this->assertEquals('foo', (string) $requests[0]->getBody()); + } + + /** + * @expectedException \Guzzle\Http\Exception\CouldNotRewindStreamException + */ + public function testThrowsExceptionWhenStreamCannotBeRewound() + { + $this->getServer()->flush(); + $this->getServer()->enqueue(array( + "HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nhi", + "HTTP/1.1 301 Moved Permanently\r\nLocation: /redirect\r\nContent-Length: 0\r\n\r\n" + )); + + $client = new Client($this->getServer()->getUrl()); + $request = $client->put(); + $request->configureRedirects(true); + $body = EntityBody::factory(fopen($this->getServer()->getUrl(), 'r')); + $body->read(1); + $request->setBody($body)->send(); + } + + public function testRedirectsCanBeDisabledPerRequest() + { + $this->getServer()->flush(); + $this->getServer()->enqueue(array("HTTP/1.1 301 Foo\r\nLocation: /foo\r\nContent-Length: 0\r\n\r\n")); + $client = new Client($this->getServer()->getUrl()); + $request = $client->put(); + $request->configureRedirects(false, 0); + $this->assertEquals(301, $request->send()->getStatusCode()); + } + + public function testCanRedirectWithNoLeadingSlashAndQuery() + { + $this->getServer()->flush(); + $this->getServer()->enqueue(array( + "HTTP/1.1 301 Moved Permanently\r\nLocation: redirect?foo=bar\r\nContent-Length: 0\r\n\r\n", + "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n", + )); + $client = new Client($this->getServer()->getUrl()); + $request = $client->get('?foo=bar'); + $request->send(); + $requests = $this->getServer()->getReceivedRequests(true); + $this->assertEquals($this->getServer()->getUrl() . '?foo=bar', $requests[0]->getUrl()); + $this->assertEquals($this->getServer()->getUrl() . 'redirect?foo=bar', $requests[1]->getUrl()); + // Ensure that the history on the actual request is correct + $this->assertEquals($this->getServer()->getUrl() . '?foo=bar', $request->getUrl()); + } + + public function testRedirectWithStrictRfc386Compliance() + { + // Flush the server and queue up a redirect followed by a successful response + $this->getServer()->flush(); + $this->getServer()->enqueue(array( + "HTTP/1.1 301 Moved Permanently\r\nLocation: redirect\r\nContent-Length: 0\r\n\r\n", + "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n" + )); + $client = new Client($this->getServer()->getUrl()); + $request = $client->get('/foo'); + $request->send(); + $requests = $this->getServer()->getReceivedRequests(true); + $this->assertEquals('/redirect', $requests[1]->getResource()); + } + + public function testResetsHistoryEachSend() + { + // Flush the server and queue up a redirect followed by a successful response + $this->getServer()->flush(); + $this->getServer()->enqueue(array( + "HTTP/1.1 301 Moved Permanently\r\nLocation: /redirect1\r\nContent-Length: 0\r\n\r\n", + "HTTP/1.1 301 Moved Permanently\r\nLocation: /redirect2\r\nContent-Length: 0\r\n\r\n", + "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n", + "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n" + )); + + // Create a client that uses the default redirect behavior + $client = new Client($this->getServer()->getUrl()); + $history = new HistoryPlugin(); + $client->addSubscriber($history); + + $request = $client->get('/foo'); + $response = $request->send(); + $this->assertEquals(3, count($history)); + $this->assertTrue($request->getParams()->hasKey('redirect.count')); + $this->assertContains('/redirect2', $response->getEffectiveUrl()); + + $request->send(); + $this->assertFalse($request->getParams()->hasKey('redirect.count')); + } + + public function testHandlesRedirectsWithSpacesProperly() + { + // Flush the server and queue up a redirect followed by a successful response + $this->getServer()->flush(); + $this->getServer()->enqueue(array( + "HTTP/1.1 301 Moved Permanently\r\nLocation: /redirect 1\r\nContent-Length: 0\r\n\r\n", + "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n" + )); + $client = new Client($this->getServer()->getUrl()); + $request = $client->get('/foo'); + $request->send(); + $reqs = $this->getServer()->getReceivedRequests(true); + $this->assertEquals('/redirect%201', $reqs[1]->getResource()); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Http/Server.php b/guzzle/guzzle/tests/Guzzle/Tests/Http/Server.php new file mode 100644 index 000000000..a61caeeeb --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Http/Server.php @@ -0,0 +1,187 @@ +port = $port ?: self::DEFAULT_PORT; + $this->client = new Client($this->getUrl()); + register_shutdown_function(array($this, 'stop')); + } + + /** + * Flush the received requests from the server + * @throws RuntimeException + */ + public function flush() + { + $this->client->delete('guzzle-server/requests')->send(); + } + + /** + * Queue an array of responses or a single response on the server. + * + * Any currently queued responses will be overwritten. Subsequent requests + * on the server will return queued responses in FIFO order. + * + * @param array|Response $responses A single or array of Responses to queue + * @throws BadResponseException + */ + public function enqueue($responses) + { + $data = array(); + foreach ((array) $responses as $response) { + + // Create the response object from a string + if (is_string($response)) { + $response = Response::fromMessage($response); + } elseif (!($response instanceof Response)) { + throw new BadResponseException('Responses must be strings or implement Response'); + } + + $data[] = array( + 'statusCode' => $response->getStatusCode(), + 'reasonPhrase' => $response->getReasonPhrase(), + 'headers' => $response->getHeaders()->toArray(), + 'body' => $response->getBody(true) + ); + } + + $request = $this->client->put('guzzle-server/responses', null, json_encode($data)); + $request->send(); + } + + /** + * Check if the server is running + * + * @return bool + */ + public function isRunning() + { + if ($this->running) { + return true; + } + + try { + $this->client->get('guzzle-server/perf', array(), array('timeout' => 5))->send(); + return $this->running = true; + } catch (\Exception $e) { + return false; + } + } + + /** + * Get the URL to the server + * + * @return string + */ + public function getUrl() + { + return 'http://127.0.0.1:' . $this->getPort() . '/'; + } + + /** + * Get the port that the server is listening on + * + * @return int + */ + public function getPort() + { + return $this->port; + } + + /** + * Get all of the received requests + * + * @param bool $hydrate Set to TRUE to turn the messages into + * actual {@see RequestInterface} objects. If $hydrate is FALSE, + * requests will be returned as strings. + * + * @return array + * @throws RuntimeException + */ + public function getReceivedRequests($hydrate = false) + { + $response = $this->client->get('guzzle-server/requests')->send(); + $data = array_filter(explode(self::REQUEST_DELIMITER, $response->getBody(true))); + if ($hydrate) { + $data = array_map(function($message) { + return RequestFactory::getInstance()->fromMessage($message); + }, $data); + } + + return $data; + } + + /** + * Start running the node.js server in the background + */ + public function start() + { + if (!$this->isRunning()) { + exec('node ' . __DIR__ . \DIRECTORY_SEPARATOR . 'server.js ' . $this->port . ' >> /tmp/server.log 2>&1 &'); + // Wait at most 5 seconds for the server the setup before proceeding + $start = time(); + while (!$this->isRunning() && time() - $start < 5); + if (!$this->running) { + throw new RuntimeException( + 'Unable to contact server.js. Have you installed node.js v0.5.0+? node must be in your path.' + ); + } + } + } + + /** + * Stop running the node.js server + */ + public function stop() + { + if (!$this->isRunning()) { + return false; + } + + $this->running = false; + $this->client->delete('guzzle-server')->send(); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Http/StaticClientTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Http/StaticClientTest.php new file mode 100644 index 000000000..091314bb7 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Http/StaticClientTest.php @@ -0,0 +1,67 @@ +assertTrue(class_exists('FooBazBar')); + $this->assertSame($client, $this->readAttribute('Guzzle\Http\StaticClient', 'client')); + } + + public function requestProvider() + { + return array_map( + function ($m) { return array($m); }, + array('GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS') + ); + } + + /** + * @dataProvider requestProvider + */ + public function testSendsRequests($method) + { + $mock = new MockPlugin(array(new Response(200))); + call_user_func('Guzzle\Http\StaticClient::' . $method, 'http://foo.com', array( + 'plugins' => array($mock) + )); + $requests = $mock->getReceivedRequests(); + $this->assertCount(1, $requests); + $this->assertEquals($method, $requests[0]->getMethod()); + } + + public function testCanCreateStreamsUsingDefaultFactory() + { + $this->getServer()->enqueue(array("HTTP/1.1 200 OK\r\nContent-Length: 4\r\n\r\ntest")); + $stream = StaticClient::get($this->getServer()->getUrl(), array('stream' => true)); + $this->assertInstanceOf('Guzzle\Stream\StreamInterface', $stream); + $this->assertEquals('test', (string) $stream); + } + + public function testCanCreateStreamsUsingCustomFactory() + { + $stream = $this->getMockBuilder('Guzzle\Stream\StreamRequestFactoryInterface') + ->setMethods(array('fromRequest')) + ->getMockForAbstractClass(); + $resource = new Stream(fopen('php://temp', 'r+')); + $stream->expects($this->once()) + ->method('fromRequest') + ->will($this->returnValue($resource)); + $this->getServer()->enqueue(array("HTTP/1.1 200 OK\r\nContent-Length: 4\r\n\r\ntest")); + $result = StaticClient::get($this->getServer()->getUrl(), array('stream' => $stream)); + $this->assertSame($resource, $result); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Http/UrlTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Http/UrlTest.php new file mode 100644 index 000000000..acba28985 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Http/UrlTest.php @@ -0,0 +1,298 @@ +assertEquals('', (string) $url); + } + + public function testPortIsDeterminedFromScheme() + { + $this->assertEquals(80, Url::factory('http://www.test.com/')->getPort()); + $this->assertEquals(443, Url::factory('https://www.test.com/')->getPort()); + $this->assertEquals(null, Url::factory('ftp://www.test.com/')->getPort()); + $this->assertEquals(8192, Url::factory('http://www.test.com:8192/')->getPort()); + } + + public function testCloneCreatesNewInternalObjects() + { + $u1 = Url::factory('http://www.test.com/'); + $u2 = clone $u1; + $this->assertNotSame($u1->getQuery(), $u2->getQuery()); + } + + public function testValidatesUrlPartsInFactory() + { + $url = Url::factory('/index.php'); + $this->assertEquals('/index.php', (string) $url); + $this->assertFalse($url->isAbsolute()); + + $url = 'http://michael:test@test.com:80/path/123?q=abc#test'; + $u = Url::factory($url); + $this->assertEquals('http://michael:test@test.com/path/123?q=abc#test', (string) $u); + $this->assertTrue($u->isAbsolute()); + } + + public function testAllowsFalsyUrlParts() + { + $url = Url::factory('http://0:50/0?0#0'); + $this->assertSame('0', $url->getHost()); + $this->assertEquals(50, $url->getPort()); + $this->assertSame('/0', $url->getPath()); + $this->assertEquals('0', (string) $url->getQuery()); + $this->assertSame('0', $url->getFragment()); + $this->assertEquals('http://0:50/0?0#0', (string) $url); + + $url = Url::factory(''); + $this->assertSame('', (string) $url); + + $url = Url::factory('0'); + $this->assertSame('0', (string) $url); + } + + public function testBuildsRelativeUrlsWithFalsyParts() + { + $url = Url::buildUrl(array( + 'host' => '0', + 'path' => '0', + )); + + $this->assertSame('//0/0', $url); + + $url = Url::buildUrl(array( + 'path' => '0', + )); + $this->assertSame('0', $url); + } + + public function testUrlStoresParts() + { + $url = Url::factory('http://test:pass@www.test.com:8081/path/path2/?a=1&b=2#fragment'); + $this->assertEquals('http', $url->getScheme()); + $this->assertEquals('test', $url->getUsername()); + $this->assertEquals('pass', $url->getPassword()); + $this->assertEquals('www.test.com', $url->getHost()); + $this->assertEquals(8081, $url->getPort()); + $this->assertEquals('/path/path2/', $url->getPath()); + $this->assertEquals('fragment', $url->getFragment()); + $this->assertEquals('a=1&b=2', (string) $url->getQuery()); + + $this->assertEquals(array( + 'fragment' => 'fragment', + 'host' => 'www.test.com', + 'pass' => 'pass', + 'path' => '/path/path2/', + 'port' => 8081, + 'query' => 'a=1&b=2', + 'scheme' => 'http', + 'user' => 'test' + ), $url->getParts()); + } + + public function testHandlesPathsCorrectly() + { + $url = Url::factory('http://www.test.com'); + $this->assertEquals('', $url->getPath()); + $url->setPath('test'); + $this->assertEquals('test', $url->getPath()); + + $url->setPath('/test/123/abc'); + $this->assertEquals(array('test', '123', 'abc'), $url->getPathSegments()); + + $parts = parse_url('http://www.test.com/test'); + $parts['path'] = ''; + $this->assertEquals('http://www.test.com', Url::buildUrl($parts)); + $parts['path'] = 'test'; + $this->assertEquals('http://www.test.com/test', Url::buildUrl($parts)); + } + + public function testAddsQueryStringIfPresent() + { + $this->assertEquals('?foo=bar', Url::buildUrl(array( + 'query' => 'foo=bar' + ))); + } + + public function testAddsToPath() + { + // Does nothing here + $this->assertEquals('http://e.com/base?a=1', (string) Url::factory('http://e.com/base?a=1')->addPath(false)); + $this->assertEquals('http://e.com/base?a=1', (string) Url::factory('http://e.com/base?a=1')->addPath('')); + $this->assertEquals('http://e.com/base?a=1', (string) Url::factory('http://e.com/base?a=1')->addPath('/')); + + $this->assertEquals('http://e.com/base/relative?a=1', (string) Url::factory('http://e.com/base?a=1')->addPath('relative')); + $this->assertEquals('http://e.com/base/relative?a=1', (string) Url::factory('http://e.com/base?a=1')->addPath('/relative')); + } + + /** + * URL combination data provider + * + * @return array + */ + public function urlCombineDataProvider() + { + return array( + array('http://www.example.com/', 'http://www.example.com/', 'http://www.example.com/'), + array('http://www.example.com/path', '/absolute', 'http://www.example.com/absolute'), + array('http://www.example.com/path', '/absolute?q=2', 'http://www.example.com/absolute?q=2'), + array('http://www.example.com/path', 'more', 'http://www.example.com/path/more'), + array('http://www.example.com/path', 'more?q=1', 'http://www.example.com/path/more?q=1'), + array('http://www.example.com/', '?q=1', 'http://www.example.com/?q=1'), + array('http://www.example.com/path', 'http://test.com', 'http://test.com'), + array('http://www.example.com:8080/path', 'http://test.com', 'http://test.com'), + array('http://www.example.com:8080/path', '?q=2#abc', 'http://www.example.com:8080/path?q=2#abc'), + array('http://u:a@www.example.com/path', 'test', 'http://u:a@www.example.com/path/test'), + array('http://www.example.com/path', 'http://u:a@www.example.com/', 'http://u:a@www.example.com/'), + array('/path?q=2', 'http://www.test.com/', 'http://www.test.com/path?q=2'), + array('http://api.flickr.com/services/', 'http://www.flickr.com/services/oauth/access_token', 'http://www.flickr.com/services/oauth/access_token'), + array('http://www.example.com/?foo=bar', 'some/path', 'http://www.example.com/some/path?foo=bar'), + array('http://www.example.com/?foo=bar', 'some/path?boo=moo', 'http://www.example.com/some/path?boo=moo&foo=bar'), + array('http://www.example.com/some/', 'path?foo=bar&foo=baz', 'http://www.example.com/some/path?foo=bar&foo=baz'), + ); + } + + /** + * @dataProvider urlCombineDataProvider + */ + public function testCombinesUrls($a, $b, $c) + { + $this->assertEquals($c, (string) Url::factory($a)->combine($b)); + } + + public function testHasGettersAndSetters() + { + $url = Url::factory('http://www.test.com/'); + $this->assertEquals('example.com', $url->setHost('example.com')->getHost()); + $this->assertEquals('8080', $url->setPort(8080)->getPort()); + $this->assertEquals('/foo/bar', $url->setPath(array('foo', 'bar'))->getPath()); + $this->assertEquals('a', $url->setPassword('a')->getPassword()); + $this->assertEquals('b', $url->setUsername('b')->getUsername()); + $this->assertEquals('abc', $url->setFragment('abc')->getFragment()); + $this->assertEquals('https', $url->setScheme('https')->getScheme()); + $this->assertEquals('a=123', (string) $url->setQuery('a=123')->getQuery()); + $this->assertEquals('https://b:a@example.com:8080/foo/bar?a=123#abc', (string) $url); + $this->assertEquals('b=boo', (string) $url->setQuery(new QueryString(array( + 'b' => 'boo' + )))->getQuery()); + $this->assertEquals('https://b:a@example.com:8080/foo/bar?b=boo#abc', (string) $url); + } + + public function testSetQueryAcceptsArray() + { + $url = Url::factory('http://www.test.com'); + $url->setQuery(array('a' => 'b')); + $this->assertEquals('http://www.test.com?a=b', (string) $url); + } + + public function urlProvider() + { + return array( + array('/foo/..', '/'), + array('//foo//..', '/'), + array('/foo/../..', '/'), + array('/foo/../.', '/'), + array('/./foo/..', '/'), + array('/./foo', '/foo'), + array('/./foo/', '/foo/'), + array('/./foo/bar/baz/pho/../..', '/foo/bar'), + array('*', '*'), + array('/foo', '/foo'), + array('/abc/123/../foo/', '/abc/foo/'), + array('/a/b/c/./../../g', '/a/g'), + array('/b/c/./../../g', '/g'), + array('/b/c/./../../g', '/g'), + array('/c/./../../g', '/g'), + array('/./../../g', '/g'), + ); + } + + /** + * @dataProvider urlProvider + */ + public function testNormalizesPaths($path, $result) + { + $url = Url::factory('http://www.example.com/'); + $url->setPath($path)->normalizePath(); + $this->assertEquals($result, $url->getPath()); + } + + public function testSettingHostWithPortModifiesPort() + { + $url = Url::factory('http://www.example.com'); + $url->setHost('foo:8983'); + $this->assertEquals('foo', $url->getHost()); + $this->assertEquals(8983, $url->getPort()); + } + + /** + * @expectedException \Guzzle\Common\Exception\InvalidArgumentException + */ + public function testValidatesUrlCanBeParsed() + { + Url::factory('foo:////'); + } + + public function testConvertsSpecialCharsInPathWhenCastingToString() + { + $url = Url::factory('http://foo.com/baz bar?a=b'); + $url->addPath('?'); + $this->assertEquals('http://foo.com/baz%20bar/%3F?a=b', (string) $url); + } + + /** + * @link http://tools.ietf.org/html/rfc3986#section-5.4.1 + */ + public function rfc3986UrlProvider() + { + $result = array( + array('g', 'http://a/b/c/g'), + array('./g', 'http://a/b/c/g'), + array('g/', 'http://a/b/c/g/'), + array('/g', 'http://a/g'), + array('?y', 'http://a/b/c/d;p?y'), + array('g?y', 'http://a/b/c/g?y'), + array('#s', 'http://a/b/c/d;p?q#s'), + array('g#s', 'http://a/b/c/g#s'), + array('g?y#s', 'http://a/b/c/g?y#s'), + array(';x', 'http://a/b/c/;x'), + array('g;x', 'http://a/b/c/g;x'), + array('g;x?y#s', 'http://a/b/c/g;x?y#s'), + array('', 'http://a/b/c/d;p?q'), + array('.', 'http://a/b/c'), + array('./', 'http://a/b/c/'), + array('..', 'http://a/b'), + array('../', 'http://a/b/'), + array('../g', 'http://a/b/g'), + array('../..', 'http://a/'), + array('../../', 'http://a/'), + array('../../g', 'http://a/g') + ); + + // This support was added in PHP 5.4.7: https://bugs.php.net/bug.php?id=62844 + if (version_compare(PHP_VERSION, '5.4.7', '>=')) { + $result[] = array('//g', 'http://g'); + } + + return $result; + } + + /** + * @dataProvider rfc3986UrlProvider + */ + public function testCombinesUrlsUsingRfc3986($relative, $result) + { + $a = Url::factory('http://a/b/c/d;p?q'); + $b = Url::factory($relative); + $this->assertEquals($result, trim((string) $a->combine($b, true), '=')); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Http/server.js b/guzzle/guzzle/tests/Guzzle/Tests/Http/server.js new file mode 100644 index 000000000..4156f1aad --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Http/server.js @@ -0,0 +1,146 @@ +/** + * Guzzle node.js test server to return queued responses to HTTP requests and + * expose a RESTful API for enqueueing responses and retrieving the requests + * that have been received. + * + * - Delete all requests that have been received: + * DELETE /guzzle-server/requests + * Host: 127.0.0.1:8124 + * + * - Enqueue responses + * PUT /guzzle-server/responses + * Host: 127.0.0.1:8124 + * + * [{ "statusCode": 200, "reasonPhrase": "OK", "headers": {}, "body": "" }] + * + * - Get the received requests + * GET /guzzle-server/requests + * Host: 127.0.0.1:8124 + * + * - Shutdown the server + * DELETE /guzzle-server + * Host: 127.0.0.1:8124 + * + * @package Guzzle PHP + * @license See the LICENSE file that was distributed with this source code. + */ + +var http = require("http"); + +/** + * Guzzle node.js server + * @class + */ +var GuzzleServer = function(port, log) { + + this.port = port; + this.log = log; + this.responses = []; + this.requests = []; + var that = this; + + var controlRequest = function(request, req, res) { + if (req.url == '/guzzle-server/perf') { + res.writeHead(200, "OK", {"Content-Length": 16}); + res.end("Body of response"); + } else if (req.method == "DELETE") { + if (req.url == "/guzzle-server/requests") { + // Clear the received requests + that.requests = []; + res.writeHead(200, "OK", { "Content-Length": 0 }); + res.end(); + if (this.log) { + console.log("Flushing requests"); + } + } else if (req.url == "/guzzle-server") { + // Shutdown the server + res.writeHead(200, "OK", { "Content-Length": 0, "Connection": "close" }); + res.end(); + if (this.log) { + console.log("Shutting down"); + } + that.server.close(); + } + } else if (req.method == "GET") { + if (req.url === "/guzzle-server/requests") { + // Get received requests + var data = that.requests.join("\n----[request]\n"); + res.writeHead(200, "OK", { "Content-Length": data.length }); + res.end(data); + if (that.log) { + console.log("Sending receiving requests"); + } + } + } else if (req.method == "PUT") { + if (req.url == "/guzzle-server/responses") { + if (that.log) { + console.log("Adding responses..."); + } + // Received response to queue + var data = request.split("\r\n\r\n")[1]; + if (!data) { + if (that.log) { + console.log("No response data was provided"); + } + res.writeHead(400, "NO RESPONSES IN REQUEST", { "Content-Length": 0 }); + } else { + that.responses = eval("(" + data + ")"); + if (that.log) { + console.log(that.responses); + } + res.writeHead(200, "OK", { "Content-Length": 0 }); + } + res.end(); + } + } + }; + + var receivedRequest = function(request, req, res) { + if (req.url.indexOf("/guzzle-server") === 0) { + controlRequest(request, req, res); + } else if (req.url.indexOf("/guzzle-server") == -1 && !that.responses.length) { + res.writeHead(500); + res.end("No responses in queue"); + } else { + var response = that.responses.shift(); + res.writeHead(response.statusCode, response.reasonPhrase, response.headers); + res.end(response.body); + that.requests.push(request); + } + }; + + this.start = function() { + + that.server = http.createServer(function(req, res) { + + var request = req.method + " " + req.url + " HTTP/" + req.httpVersion + "\r\n"; + for (var i in req.headers) { + request += i + ": " + req.headers[i] + "\r\n"; + } + request += "\r\n"; + + // Receive each chunk of the request body + req.addListener("data", function(chunk) { + request += chunk; + }); + + // Called when the request completes + req.addListener("end", function() { + receivedRequest(request, req, res); + }); + }); + that.server.listen(port, "127.0.0.1"); + + if (this.log) { + console.log("Server running at http://127.0.0.1:8124/"); + } + }; +}; + +// Get the port from the arguments +port = process.argv.length >= 3 ? process.argv[2] : 8124; +log = process.argv.length >= 4 ? process.argv[3] : false; + +// Start the server +server = new GuzzleServer(port, log); +server.start(); diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Inflection/InflectorTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Inflection/InflectorTest.php new file mode 100644 index 000000000..990c0af66 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Inflection/InflectorTest.php @@ -0,0 +1,37 @@ +assertSame(Inflector::getDefault(), Inflector::getDefault()); + } + + public function testSnake() + { + $this->assertEquals('camel_case', Inflector::getDefault()->snake('camelCase')); + $this->assertEquals('camel_case', Inflector::getDefault()->snake('CamelCase')); + $this->assertEquals('camel_case_words', Inflector::getDefault()->snake('CamelCaseWords')); + $this->assertEquals('camel_case_words', Inflector::getDefault()->snake('CamelCase_words')); + $this->assertEquals('test', Inflector::getDefault()->snake('test')); + $this->assertEquals('test', Inflector::getDefault()->snake('test')); + $this->assertEquals('expect100_continue', Inflector::getDefault()->snake('Expect100Continue')); + } + + public function testCamel() + { + $this->assertEquals('CamelCase', Inflector::getDefault()->camel('camel_case')); + $this->assertEquals('CamelCaseWords', Inflector::getDefault()->camel('camel_case_words')); + $this->assertEquals('Test', Inflector::getDefault()->camel('test')); + $this->assertEquals('Expect100Continue', ucfirst(Inflector::getDefault()->camel('expect100_continue'))); + // Get from cache + $this->assertEquals('Test', Inflector::getDefault()->camel('test', false)); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Inflection/MemoizingInflectorTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Inflection/MemoizingInflectorTest.php new file mode 100644 index 000000000..f00b7fad8 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Inflection/MemoizingInflectorTest.php @@ -0,0 +1,46 @@ +getMock('Guzzle\Inflection\Inflector', array('snake', 'camel')); + $mock->expects($this->once())->method('snake')->will($this->returnValue('foo_bar')); + $mock->expects($this->once())->method('camel')->will($this->returnValue('FooBar')); + + $inflector = new MemoizingInflector($mock); + $this->assertEquals('foo_bar', $inflector->snake('FooBar')); + $this->assertEquals('foo_bar', $inflector->snake('FooBar')); + $this->assertEquals('FooBar', $inflector->camel('foo_bar')); + $this->assertEquals('FooBar', $inflector->camel('foo_bar')); + } + + public function testProtectsAgainstCacheOverflow() + { + $inflector = new MemoizingInflector(new Inflector(), 10); + for ($i = 1; $i < 11; $i++) { + $inflector->camel('foo_' . $i); + $inflector->snake('Foo' . $i); + } + + $cache = $this->readAttribute($inflector, 'cache'); + $this->assertEquals(10, count($cache['snake'])); + $this->assertEquals(10, count($cache['camel'])); + + $inflector->camel('baz!'); + $inflector->snake('baz!'); + + // Now ensure that 20% of the cache was removed (2), then the item was added + $cache = $this->readAttribute($inflector, 'cache'); + $this->assertEquals(9, count($cache['snake'])); + $this->assertEquals(9, count($cache['camel'])); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Inflection/PreComputedInflectorTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Inflection/PreComputedInflectorTest.php new file mode 100644 index 000000000..ff2654cf6 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Inflection/PreComputedInflectorTest.php @@ -0,0 +1,45 @@ +getMock('Guzzle\Inflection\Inflector', array('snake', 'camel')); + $mock->expects($this->once())->method('snake')->with('Test')->will($this->returnValue('test')); + $mock->expects($this->once())->method('camel')->with('Test')->will($this->returnValue('Test')); + $inflector = new PreComputedInflector($mock, array('FooBar' => 'foo_bar'), array('foo_bar' => 'FooBar')); + $this->assertEquals('FooBar', $inflector->camel('foo_bar')); + $this->assertEquals('foo_bar', $inflector->snake('FooBar')); + $this->assertEquals('Test', $inflector->camel('Test')); + $this->assertEquals('test', $inflector->snake('Test')); + } + + public function testMirrorsPrecomputedValues() + { + $mock = $this->getMock('Guzzle\Inflection\Inflector', array('snake', 'camel')); + $mock->expects($this->never())->method('snake'); + $mock->expects($this->never())->method('camel'); + $inflector = new PreComputedInflector($mock, array('Zeep' => 'zeep'), array(), true); + $this->assertEquals('Zeep', $inflector->camel('zeep')); + $this->assertEquals('zeep', $inflector->snake('Zeep')); + } + + public function testMirrorsPrecomputedValuesByMerging() + { + $mock = $this->getMock('Guzzle\Inflection\Inflector', array('snake', 'camel')); + $mock->expects($this->never())->method('snake'); + $mock->expects($this->never())->method('camel'); + $inflector = new PreComputedInflector($mock, array('Zeep' => 'zeep'), array('foo' => 'Foo'), true); + $this->assertEquals('Zeep', $inflector->camel('zeep')); + $this->assertEquals('zeep', $inflector->snake('Zeep')); + $this->assertEquals('Foo', $inflector->camel('foo')); + $this->assertEquals('foo', $inflector->snake('Foo')); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Iterator/AppendIteratorTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Iterator/AppendIteratorTest.php new file mode 100644 index 000000000..8d6ae845a --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Iterator/AppendIteratorTest.php @@ -0,0 +1,29 @@ + 1, + 'b' => 2 + )); + $b = new \ArrayIterator(array()); + $c = new \ArrayIterator(array( + 'c' => 3, + 'd' => 4 + )); + $i = new AppendIterator(); + $i->append($a); + $i->append($b); + $i->append($c); + $this->assertEquals(array(1, 2, 3, 4), iterator_to_array($i, false)); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Iterator/ChunkedIteratorTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Iterator/ChunkedIteratorTest.php new file mode 100644 index 000000000..5d474b557 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Iterator/ChunkedIteratorTest.php @@ -0,0 +1,60 @@ +assertEquals(11, count($chunks)); + foreach ($chunks as $j => $chunk) { + $this->assertEquals(range($j * 10, min(100, $j * 10 + 9)), $chunk); + } + } + + public function testChunksIteratorWithOddValues() + { + $chunked = new ChunkedIterator(new \ArrayIterator(array(1, 2, 3, 4, 5)), 2); + $chunks = iterator_to_array($chunked, false); + $this->assertEquals(3, count($chunks)); + $this->assertEquals(array(1, 2), $chunks[0]); + $this->assertEquals(array(3, 4), $chunks[1]); + $this->assertEquals(array(5), $chunks[2]); + } + + /** + * @test + * @runInSeparateProcess + */ + public function mustNotTerminateWithTraversable() + { + $traversable = simplexml_load_string('')->foo; + $chunked = new ChunkedIterator($traversable, 2); + $actual = iterator_to_array($chunked, false); + $this->assertCount(2, $actual); + } + + /** + * @test + */ + public function sizeOfZeroMakesIteratorInvalid() { + $chunked = new ChunkedIterator(new \ArrayIterator(range(1, 5)), 0); + $chunked->rewind(); + $this->assertFalse($chunked->valid()); + } + + /** + * @test + * @expectedException \InvalidArgumentException + */ + public function sizeLowerZeroThrowsException() { + $chunked = new ChunkedIterator(new \ArrayIterator(range(1, 5)), -1); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Iterator/FilterIteratorTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Iterator/FilterIteratorTest.php new file mode 100644 index 000000000..73b4f6987 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Iterator/FilterIteratorTest.php @@ -0,0 +1,28 @@ +assertEquals(range(1, 99, 2), iterator_to_array($i, false)); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testValidatesCallable() + { + $i = new FilterIterator(new \ArrayIterator(), new \stdClass()); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Iterator/MapIteratorTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Iterator/MapIteratorTest.php new file mode 100644 index 000000000..4de4a6bc1 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Iterator/MapIteratorTest.php @@ -0,0 +1,28 @@ +assertEquals(range(0, 1000, 10), iterator_to_array($i, false)); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testValidatesCallable() + { + $i = new MapIterator(new \ArrayIterator(), new \stdClass()); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Iterator/MethodProxyIteratorTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Iterator/MethodProxyIteratorTest.php new file mode 100644 index 000000000..5bcf06fb0 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Iterator/MethodProxyIteratorTest.php @@ -0,0 +1,28 @@ +append('a'); + $proxy->append('b'); + $this->assertEquals(array('a', 'b'), $i->getArrayCopy()); + $this->assertEquals(array('a', 'b'), $proxy->getArrayCopy()); + } + + public function testUsesInnerIterator() + { + $i = new MethodProxyIterator(new ChunkedIterator(new \ArrayIterator(array(1, 2, 3, 4, 5)), 2)); + $this->assertEquals(3, count(iterator_to_array($i, false))); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Log/ArrayLogAdapterTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Log/ArrayLogAdapterTest.php new file mode 100644 index 000000000..95033e0a7 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Log/ArrayLogAdapterTest.php @@ -0,0 +1,23 @@ +log('test', \LOG_NOTICE, 'localhost'); + $this->assertEquals(array(array('message' => 'test', 'priority' => \LOG_NOTICE, 'extras' => 'localhost')), $adapter->getLogs()); + } + + public function testClearLog() + { + $adapter = new ArrayLogAdapter(); + $adapter->log('test', \LOG_NOTICE, 'localhost'); + $adapter->clearLogs(); + $this->assertEquals(array(), $adapter->getLogs()); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Log/ClosureLogAdapterTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Log/ClosureLogAdapterTest.php new file mode 100644 index 000000000..7d1aa1609 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Log/ClosureLogAdapterTest.php @@ -0,0 +1,30 @@ +adapter = new ClosureLogAdapter(function($message, $priority, $extras = null) use ($that, &$modified) { + $modified = array($message, $priority, $extras); + }); + $this->adapter->log('test', LOG_NOTICE, 'localhost'); + $this->assertEquals(array('test', LOG_NOTICE, 'localhost'), $modified); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testThrowsExceptionWhenNotCallable() + { + $this->adapter = new ClosureLogAdapter(123); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Log/MessageFormatterTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Log/MessageFormatterTest.php new file mode 100644 index 000000000..385172443 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Log/MessageFormatterTest.php @@ -0,0 +1,143 @@ +request = new EntityEnclosingRequest('POST', 'http://foo.com?q=test', array( + 'X-Foo' => 'bar', + 'Authorization' => 'Baz' + )); + $this->request->setBody(EntityBody::factory('Hello')); + + $this->response = new Response(200, array( + 'X-Test' => 'Abc' + ), 'Foo'); + + $this->handle = $this->getMockBuilder('Guzzle\Http\Curl\CurlHandle') + ->disableOriginalConstructor() + ->setMethods(array('getError', 'getErrorNo', 'getStderr', 'getInfo')) + ->getMock(); + + $this->handle->expects($this->any()) + ->method('getError') + ->will($this->returnValue('e')); + + $this->handle->expects($this->any()) + ->method('getErrorNo') + ->will($this->returnValue('123')); + + $this->handle->expects($this->any()) + ->method('getStderr') + ->will($this->returnValue('testing')); + + $this->handle->expects($this->any()) + ->method('getInfo') + ->will($this->returnValueMap(array( + array(CURLINFO_CONNECT_TIME, '123'), + array(CURLINFO_TOTAL_TIME, '456') + ))); + } + + public function logProvider() + { + return array( + // Uses the cache for the second time + array('{method} - {method}', 'POST - POST'), + array('{url}', 'http://foo.com?q=test'), + array('{port}', '80'), + array('{resource}', '/?q=test'), + array('{host}', 'foo.com'), + array('{hostname}', gethostname()), + array('{protocol}/{version}', 'HTTP/1.1'), + array('{code} {phrase}', '200 OK'), + array('{req_header_Foo}', ''), + array('{req_header_X-Foo}', 'bar'), + array('{req_header_Authorization}', 'Baz'), + array('{res_header_foo}', ''), + array('{res_header_X-Test}', 'Abc'), + array('{req_body}', 'Hello'), + array('{res_body}', 'Foo'), + array('{curl_stderr}', 'testing'), + array('{curl_error}', 'e'), + array('{curl_code}', '123'), + array('{connect_time}', '123'), + array('{total_time}', '456') + ); + } + + /** + * @dataProvider logProvider + */ + public function testFormatsMessages($template, $output) + { + $formatter = new MessageFormatter($template); + $this->assertEquals($output, $formatter->format($this->request, $this->response, $this->handle)); + } + + public function testFormatsRequestsAndResponses() + { + $formatter = new MessageFormatter(); + $formatter->setTemplate('{request}{response}'); + $this->assertEquals($this->request . $this->response, $formatter->format($this->request, $this->response)); + } + + public function testAddsTimestamp() + { + $formatter = new MessageFormatter('{ts}'); + $this->assertNotEmpty($formatter->format($this->request, $this->response)); + } + + public function testUsesResponseWhenNoHandleAndGettingCurlInformation() + { + $formatter = new MessageFormatter('{connect_time}/{total_time}'); + $response = $this->getMockBuilder('Guzzle\Http\Message\Response') + ->disableOriginalConstructor() + ->setMethods(array('getInfo')) + ->getMock(); + $response->expects($this->exactly(2)) + ->method('getInfo') + ->will($this->returnValueMap(array( + array('connect_time', '1'), + array('total_time', '2'), + ))); + $this->assertEquals('1/2', $formatter->format($this->request, $response)); + } + + public function testUsesEmptyStringWhenNoHandleAndNoResponse() + { + $formatter = new MessageFormatter('{connect_time}/{total_time}'); + $this->assertEquals('/', $formatter->format($this->request)); + } + + public function testInjectsTotalTime() + { + $out = ''; + $formatter = new MessageFormatter('{connect_time}/{total_time}'); + $adapter = new ClosureLogAdapter(function ($m) use (&$out) { $out .= $m; }); + $log = new LogPlugin($adapter, $formatter); + $this->getServer()->enqueue("HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nHI"); + $client = new Client($this->getServer()->getUrl()); + $client->addSubscriber($log); + $client->get('/')->send(); + $this->assertNotEquals('/', $out); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Log/PsrLogAdapterTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Log/PsrLogAdapterTest.php new file mode 100644 index 000000000..7b72dd6a0 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Log/PsrLogAdapterTest.php @@ -0,0 +1,25 @@ +pushHandler($handler); + $adapter = new PsrLogAdapter($log); + $adapter->log('test!', LOG_INFO); + $this->assertTrue($handler->hasInfoRecords()); + $this->assertSame($log, $adapter->getLogObject()); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Log/Zf2LogAdapterTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Log/Zf2LogAdapterTest.php new file mode 100644 index 000000000..1b6128365 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Log/Zf2LogAdapterTest.php @@ -0,0 +1,51 @@ +stream = fopen('php://temp', 'r+'); + $this->log = new Logger(); + $this->log->addWriter(new Stream($this->stream)); + $this->adapter = new Zf2LogAdapter($this->log); + + } + + public function testLogsMessagesToAdaptedObject() + { + // Test without a priority + $this->adapter->log('Zend_Test!', \LOG_NOTICE); + rewind($this->stream); + $contents = stream_get_contents($this->stream); + $this->assertEquals(1, substr_count($contents, 'Zend_Test!')); + + // Test with a priority + $this->adapter->log('Zend_Test!', \LOG_ALERT); + rewind($this->stream); + $contents = stream_get_contents($this->stream); + $this->assertEquals(2, substr_count($contents, 'Zend_Test!')); + } + + public function testExposesAdaptedLogObject() + { + $this->assertEquals($this->log, $this->adapter->getLogObject()); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Mock/CustomResponseModel.php b/guzzle/guzzle/tests/Guzzle/Tests/Mock/CustomResponseModel.php new file mode 100644 index 000000000..3fb6527be --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Mock/CustomResponseModel.php @@ -0,0 +1,21 @@ +command = $command; + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Mock/ErrorResponseMock.php b/guzzle/guzzle/tests/Guzzle/Tests/Mock/ErrorResponseMock.php new file mode 100644 index 000000000..aabb15f9c --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Mock/ErrorResponseMock.php @@ -0,0 +1,25 @@ +command = $command; + $this->response = $response; + $this->message = 'Error from ' . $response; + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Mock/ExceptionMock.php b/guzzle/guzzle/tests/Guzzle/Tests/Mock/ExceptionMock.php new file mode 100644 index 000000000..97a197487 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Mock/ExceptionMock.php @@ -0,0 +1,11 @@ +multiHandle; + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Mock/MockObserver.php b/guzzle/guzzle/tests/Guzzle/Tests/Mock/MockObserver.php new file mode 100644 index 000000000..11e22eb46 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Mock/MockObserver.php @@ -0,0 +1,65 @@ +events as $event) { + if ($event->getName() == $eventName) { + return true; + } + } + + return false; + } + + public function getLastEvent() + { + return end($this->events); + } + + public function count() + { + return count($this->events); + } + + public function getGrouped() + { + $events = array(); + foreach ($this->events as $event) { + if (!isset($events[$event->getName()])) { + $events[$event->getName()] = array(); + } + $events[$event->getName()][] = $event; + } + + return $events; + } + + public function getData($event, $key, $occurrence = 0) + { + $grouped = $this->getGrouped(); + if (isset($grouped[$event])) { + return $grouped[$event][$occurrence][$key]; + } + + return null; + } + + public function update(Event $event) + { + $this->events[] = $event; + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Mock/MockSubject.php b/guzzle/guzzle/tests/Guzzle/Tests/Mock/MockSubject.php new file mode 100644 index 000000000..e011959bb --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Mock/MockSubject.php @@ -0,0 +1,7 @@ + 'allseeing-i.com', + 'path' => '/', + 'data' => array( + 'PHPSESSID' => '6c951590e7a9359bcedde25cda73e43c' + ), + 'max_age' => NULL, + 'expires' => 'Sat, 26-Jul-2008 17:00:42 GMT', + 'version' => NULL, + 'secure' => NULL, + 'discard' => NULL, + 'port' => NULL, + 'cookies' => array( + 'ASIHTTPRequestTestCookie' => 'This+is+the+value' + ), + 'comment' => null, + 'comment_url' => null, + 'http_only' => false + ) + ), + array('', false), + array('foo', false), + // Test setting a blank value for a cookie + array(array( + 'foo=', 'foo =', 'foo =;', 'foo= ;', 'foo =', 'foo= '), + array( + 'cookies' => array( + 'foo' => '' + ), + 'data' => array(), + 'discard' => null, + 'domain' => null, + 'expires' => null, + 'max_age' => null, + 'path' => '/', + 'port' => null, + 'secure' => null, + 'version' => null, + 'comment' => null, + 'comment_url' => null, + 'http_only' => false + ) + ), + // Test setting a value and removing quotes + array(array( + 'foo=1', 'foo =1', 'foo =1;', 'foo=1 ;', 'foo =1', 'foo= 1', 'foo = 1 ;', 'foo="1"', 'foo="1";', 'foo= "1";'), + array( + 'cookies' => array( + 'foo' => '1' + ), + 'data' => array(), + 'discard' => null, + 'domain' => null, + 'expires' => null, + 'max_age' => null, + 'path' => '/', + 'port' => null, + 'secure' => null, + 'version' => null, + 'comment' => null, + 'comment_url' => null, + 'http_only' => false + ) + ), + // Test setting multiple values + array(array( + 'foo=1; bar=2;', 'foo =1; bar = "2"', 'foo=1; bar=2'), + array( + 'cookies' => array( + 'foo' => '1', + 'bar' => '2', + ), + 'data' => array(), + 'discard' => null, + 'domain' => null, + 'expires' => null, + 'max_age' => null, + 'path' => '/', + 'port' => null, + 'secure' => null, + 'version' => null, + 'comment' => null, + 'comment_url' => null, + 'http_only' => false + ) + ), + // Tests getting the domain and path from a reference request + array(array( + 'foo=1; port="80,8081"; httponly', 'foo=1; port="80,8081"; domain=www.test.com; HttpOnly;', 'foo=1; ; domain=www.test.com; path=/path/; port="80,8081"; HttpOnly;'), + array( + 'cookies' => array( + 'foo' => 1 + ), + 'data' => array(), + 'discard' => null, + 'domain' => 'www.test.com', + 'expires' => null, + 'max_age' => null, + 'path' => '/path/', + 'port' => array('80', '8081'), + 'secure' => null, + 'version' => null, + 'comment' => null, + 'comment_url' => null, + 'http_only' => true + ), + 'http://www.test.com/path/' + ), + // Some of the following tests are based on http://framework.zend.com/svn/framework/standard/trunk/tests/Zend/Http/CookieTest.php + array( + 'justacookie=foo; domain=example.com', + array( + 'cookies' => array( + 'justacookie' => 'foo' + ), + 'domain' => 'example.com', + 'path' => '', + 'data' => array(), + 'discard' => null, + 'expires' => null, + 'max_age' => null, + 'path' => '/', + 'port' => null, + 'secure' => null, + 'version' => null, + 'comment' => null, + 'comment_url' => null, + 'http_only' => false + ) + ), + array( + 'expires=tomorrow; secure; path=/Space Out/; expires=Tue, 21-Nov-2006 08:33:44 GMT; domain=.example.com', + array( + 'cookies' => array( + 'expires' => 'tomorrow' + ), + 'domain' => '.example.com', + 'path' => '/Space Out/', + 'expires' => 'Tue, 21-Nov-2006 08:33:44 GMT', + 'data' => array(), + 'discard' => null, + 'port' => null, + 'secure' => true, + 'version' => null, + 'max_age' => null, + 'comment' => null, + 'comment_url' => null, + 'http_only' => false + ) + ), + array( + 'domain=unittests; expires=Tue, 21-Nov-2006 08:33:44 GMT; domain=example.com; path=/some value/', + array( + 'cookies' => array( + 'domain' => 'unittests' + ), + 'domain' => 'example.com', + 'path' => '/some value/', + 'expires' => 'Tue, 21-Nov-2006 08:33:44 GMT', + 'secure' => false, + 'data' => array(), + 'discard' => null, + 'max_age' => null, + 'port' => null, + 'version' => null, + 'comment' => null, + 'comment_url' => null, + 'http_only' => false + ) + ), + array( + 'path=indexAction; path=/; domain=.foo.com; expires=Tue, 21-Nov-2006 08:33:44 GMT', + array( + 'cookies' => array( + 'path' => 'indexAction' + ), + 'domain' => '.foo.com', + 'path' => '/', + 'expires' => 'Tue, 21-Nov-2006 08:33:44 GMT', + 'secure' => false, + 'data' => array(), + 'discard' => null, + 'max_age' => null, + 'port' => null, + 'version' => null, + 'comment' => null, + 'comment_url' => null, + 'http_only' => false + ) + ), + array( + 'secure=sha1; secure; SECURE; domain=some.really.deep.domain.com; version=1; Max-Age=86400', + array( + 'cookies' => array( + 'secure' => 'sha1' + ), + 'domain' => 'some.really.deep.domain.com', + 'path' => '/', + 'secure' => true, + 'data' => array(), + 'discard' => null, + 'expires' => time() + 86400, + 'max_age' => 86400, + 'port' => null, + 'version' => 1, + 'comment' => null, + 'comment_url' => null, + 'http_only' => false + ) + ), + array( + 'PHPSESSID=123456789+abcd%2Cef; secure; discard; domain=.localdomain; path=/foo/baz; expires=Tue, 21-Nov-2006 08:33:44 GMT;', + array( + 'cookies' => array( + 'PHPSESSID' => '123456789+abcd%2Cef' + ), + 'domain' => '.localdomain', + 'path' => '/foo/baz', + 'expires' => 'Tue, 21-Nov-2006 08:33:44 GMT', + 'secure' => true, + 'data' => array(), + 'discard' => true, + 'max_age' => null, + 'port' => null, + 'version' => null, + 'comment' => null, + 'comment_url' => null, + 'http_only' => false + ) + ), + ); + } + + /** + * @dataProvider cookieParserDataProvider + */ + public function testParseCookie($cookie, $parsed, $url = null) + { + $c = $this->cookieParserClass; + $parser = new $c(); + + $request = null; + if ($url) { + $url = Url::factory($url); + $host = $url->getHost(); + $path = $url->getPath(); + } else { + $host = ''; + $path = ''; + } + + foreach ((array) $cookie as $c) { + $p = $parser->parseCookie($c, $host, $path); + + // Remove expires values from the assertion if they are relatively equal by allowing a 5 minute difference + if ($p['expires'] != $parsed['expires']) { + if (abs($p['expires'] - $parsed['expires']) < 300) { + unset($p['expires']); + unset($parsed['expires']); + } + } + + if (is_array($parsed)) { + foreach ($parsed as $key => $value) { + $this->assertEquals($parsed[$key], $p[$key], 'Comparing ' . $key . ' ' . var_export($value, true) . ' : ' . var_export($parsed, true) . ' | ' . var_export($p, true)); + } + + foreach ($p as $key => $value) { + $this->assertEquals($p[$key], $parsed[$key], 'Comparing ' . $key . ' ' . var_export($value, true) . ' : ' . var_export($parsed, true) . ' | ' . var_export($p, true)); + } + } else { + $this->assertEquals($parsed, $p); + } + } + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Parser/Cookie/CookieParserTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Parser/Cookie/CookieParserTest.php new file mode 100644 index 000000000..75d336fa5 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Parser/Cookie/CookieParserTest.php @@ -0,0 +1,22 @@ +parseCookie('foo=baz+bar', null, null, true); + $this->assertEquals(array( + 'foo' => 'baz bar' + ), $result['cookies']); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Parser/Message/MessageParserProvider.php b/guzzle/guzzle/tests/Guzzle/Tests/Parser/Message/MessageParserProvider.php new file mode 100644 index 000000000..da58bb465 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Parser/Message/MessageParserProvider.php @@ -0,0 +1,225 @@ + 'GET', + 'protocol' => 'HTTP', + 'version' => '1.1', + 'request_url' => array( + 'scheme' => 'http', + 'host' => '', + 'port' => '', + 'path' => '/', + 'query' => '' + ), + 'headers' => array(), + 'body' => '' + )), + // Path and query string, multiple header values per header and case sensitive storage + array("HEAD /path?query=foo HTTP/1.0\r\nHost: example.com\r\nX-Foo: foo\r\nx-foo: Bar\r\nX-Foo: foo\r\nX-Foo: Baz\r\n\r\n", array( + 'method' => 'HEAD', + 'protocol' => 'HTTP', + 'version' => '1.0', + 'request_url' => array( + 'scheme' => 'http', + 'host' => 'example.com', + 'port' => '', + 'path' => '/path', + 'query' => 'query=foo' + ), + 'headers' => array( + 'Host' => 'example.com', + 'X-Foo' => array('foo', 'foo', 'Baz'), + 'x-foo' => 'Bar' + ), + 'body' => '' + )), + // Includes a body + array("PUT / HTTP/1.0\r\nhost: example.com:443\r\nContent-Length: 4\r\n\r\ntest", array( + 'method' => 'PUT', + 'protocol' => 'HTTP', + 'version' => '1.0', + 'request_url' => array( + 'scheme' => 'https', + 'host' => 'example.com', + 'port' => '443', + 'path' => '/', + 'query' => '' + ), + 'headers' => array( + 'host' => 'example.com:443', + 'Content-Length' => '4' + ), + 'body' => 'test' + )), + // Includes Authorization headers + array("GET / HTTP/1.1\r\nHost: example.com:8080\r\nAuthorization: Basic {$auth}\r\n\r\n", array( + 'method' => 'GET', + 'protocol' => 'HTTP', + 'version' => '1.1', + 'request_url' => array( + 'scheme' => 'http', + 'host' => 'example.com', + 'port' => '8080', + 'path' => '/', + 'query' => '' + ), + 'headers' => array( + 'Host' => 'example.com:8080', + 'Authorization' => "Basic {$auth}" + ), + 'body' => '' + )), + // Include authorization header + array("GET / HTTP/1.1\r\nHost: example.com:8080\r\nauthorization: Basic {$auth}\r\n\r\n", array( + 'method' => 'GET', + 'protocol' => 'HTTP', + 'version' => '1.1', + 'request_url' => array( + 'scheme' => 'http', + 'host' => 'example.com', + 'port' => '8080', + 'path' => '/', + 'query' => '' + ), + 'headers' => array( + 'Host' => 'example.com:8080', + 'authorization' => "Basic {$auth}" + ), + 'body' => '' + )), + ); + } + + public function responseProvider() + { + return array( + // Empty request + array('', false), + + array("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n", array( + 'protocol' => 'HTTP', + 'version' => '1.1', + 'code' => '200', + 'reason_phrase' => 'OK', + 'headers' => array( + 'Content-Length' => 0 + ), + 'body' => '' + )), + array("HTTP/1.0 400 Bad Request\r\nContent-Length: 0\r\n\r\n", array( + 'protocol' => 'HTTP', + 'version' => '1.0', + 'code' => '400', + 'reason_phrase' => 'Bad Request', + 'headers' => array( + 'Content-Length' => 0 + ), + 'body' => '' + )), + array("HTTP/1.0 100 Continue\r\n\r\n", array( + 'protocol' => 'HTTP', + 'version' => '1.0', + 'code' => '100', + 'reason_phrase' => 'Continue', + 'headers' => array(), + 'body' => '' + )), + array("HTTP/1.1 204 No Content\r\nX-Foo: foo\r\nx-foo: Bar\r\nX-Foo: foo\r\n\r\n", array( + 'protocol' => 'HTTP', + 'version' => '1.1', + 'code' => '204', + 'reason_phrase' => 'No Content', + 'headers' => array( + 'X-Foo' => array('foo', 'foo'), + 'x-foo' => 'Bar' + ), + 'body' => '' + )), + array("HTTP/1.1 200 Ok that is great!\r\nContent-Length: 4\r\n\r\nTest", array( + 'protocol' => 'HTTP', + 'version' => '1.1', + 'code' => '200', + 'reason_phrase' => 'Ok that is great!', + 'headers' => array( + 'Content-Length' => 4 + ), + 'body' => 'Test' + )), + ); + } + + public function compareRequestResults($result, $expected) + { + if (!$result) { + $this->assertFalse($expected); + return; + } + + $this->assertEquals($result['method'], $expected['method']); + $this->assertEquals($result['protocol'], $expected['protocol']); + $this->assertEquals($result['version'], $expected['version']); + $this->assertEquals($result['request_url'], $expected['request_url']); + $this->assertEquals($result['body'], $expected['body']); + $this->compareHttpHeaders($result['headers'], $expected['headers']); + } + + public function compareResponseResults($result, $expected) + { + if (!$result) { + $this->assertFalse($expected); + return; + } + + $this->assertEquals($result['protocol'], $expected['protocol']); + $this->assertEquals($result['version'], $expected['version']); + $this->assertEquals($result['code'], $expected['code']); + $this->assertEquals($result['reason_phrase'], $expected['reason_phrase']); + $this->assertEquals($result['body'], $expected['body']); + $this->compareHttpHeaders($result['headers'], $expected['headers']); + } + + protected function normalizeHeaders($headers) + { + $normalized = array(); + foreach ($headers as $key => $value) { + $key = strtolower($key); + if (!isset($normalized[$key])) { + $normalized[$key] = $value; + } elseif (!is_array($normalized[$key])) { + $normalized[$key] = array($value); + } else { + $normalized[$key][] = $value; + } + } + + foreach ($normalized as $key => &$value) { + if (is_array($value)) { + sort($value); + } + } + + return $normalized; + } + + public function compareHttpHeaders($result, $expected) + { + // Aggregate all headers case-insensitively + $result = $this->normalizeHeaders($result); + $expected = $this->normalizeHeaders($expected); + $this->assertEquals($result, $expected); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Parser/Message/MessageParserTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Parser/Message/MessageParserTest.php new file mode 100644 index 000000000..2f5222893 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Parser/Message/MessageParserTest.php @@ -0,0 +1,58 @@ +compareRequestResults($parts, $parser->parseRequest($message)); + } + + /** + * @dataProvider responseProvider + */ + public function testParsesResponses($message, $parts) + { + $parser = new MessageParser(); + $this->compareResponseResults($parts, $parser->parseResponse($message)); + } + + public function testParsesRequestsWithMissingProtocol() + { + $parser = new MessageParser(); + $parts = $parser->parseRequest("GET /\r\nHost: Foo.com\r\n\r\n"); + $this->assertEquals('GET', $parts['method']); + $this->assertEquals('HTTP', $parts['protocol']); + $this->assertEquals('1.1', $parts['version']); + } + + public function testParsesRequestsWithMissingVersion() + { + $parser = new MessageParser(); + $parts = $parser->parseRequest("GET / HTTP\r\nHost: Foo.com\r\n\r\n"); + $this->assertEquals('GET', $parts['method']); + $this->assertEquals('HTTP', $parts['protocol']); + $this->assertEquals('1.1', $parts['version']); + } + + public function testParsesResponsesWithMissingReasonPhrase() + { + $parser = new MessageParser(); + $parts = $parser->parseResponse("HTTP/1.1 200\r\n\r\n"); + $this->assertEquals('200', $parts['code']); + $this->assertEquals('', $parts['reason_phrase']); + $this->assertEquals('HTTP', $parts['protocol']); + $this->assertEquals('1.1', $parts['version']); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Parser/Message/PeclHttpMessageParserTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Parser/Message/PeclHttpMessageParserTest.php new file mode 100644 index 000000000..6706e2063 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Parser/Message/PeclHttpMessageParserTest.php @@ -0,0 +1,36 @@ +markTestSkipped('pecl_http is not available.'); + } + } + + /** + * @dataProvider requestProvider + */ + public function testParsesRequests($message, $parts) + { + $parser = new PeclHttpMessageParser(); + $this->compareRequestResults($parts, $parser->parseRequest($message)); + } + + /** + * @dataProvider responseProvider + */ + public function testParsesResponses($message, $parts) + { + $parser = new PeclHttpMessageParser(); + $this->compareResponseResults($parts, $parser->parseResponse($message)); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Parser/ParserRegistryTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Parser/ParserRegistryTest.php new file mode 100644 index 000000000..7675efb96 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Parser/ParserRegistryTest.php @@ -0,0 +1,33 @@ +registerParser('foo', $c); + $this->assertSame($c, $r->getParser('foo')); + } + + public function testReturnsNullWhenNotFound() + { + $r = new ParserRegistry(); + $this->assertNull($r->getParser('FOO')); + } + + public function testReturnsLazyLoadedDefault() + { + $r = new ParserRegistry(); + $c = $r->getParser('cookie'); + $this->assertInstanceOf('Guzzle\Parser\Cookie\CookieParser', $c); + $this->assertSame($c, $r->getParser('cookie')); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Parser/UriTemplate/AbstractUriTemplateTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Parser/UriTemplate/AbstractUriTemplateTest.php new file mode 100644 index 000000000..a05fc2e4d --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Parser/UriTemplate/AbstractUriTemplateTest.php @@ -0,0 +1,113 @@ + 'value', + 'hello' => 'Hello World!', + 'empty' => '', + 'path' => '/foo/bar', + 'x' => '1024', + 'y' => '768', + 'null' => null, + 'list' => array('red', 'green', 'blue'), + 'keys' => array( + "semi" => ';', + "dot" => '.', + "comma" => ',' + ), + 'empty_keys' => array(), + ); + + return array_map(function($t) use ($params) { + $t[] = $params; + return $t; + }, array( + array('foo', 'foo'), + array('{var}', 'value'), + array('{hello}', 'Hello%20World%21'), + array('{+var}', 'value'), + array('{+hello}', 'Hello%20World!'), + array('{+path}/here', '/foo/bar/here'), + array('here?ref={+path}', 'here?ref=/foo/bar'), + array('X{#var}', 'X#value'), + array('X{#hello}', 'X#Hello%20World!'), + array('map?{x,y}', 'map?1024,768'), + array('{x,hello,y}', '1024,Hello%20World%21,768'), + array('{+x,hello,y}', '1024,Hello%20World!,768'), + array('{+path,x}/here', '/foo/bar,1024/here'), + array('{#x,hello,y}', '#1024,Hello%20World!,768'), + array('{#path,x}/here', '#/foo/bar,1024/here'), + array('X{.var}', 'X.value'), + array('X{.x,y}', 'X.1024.768'), + array('{/var}', '/value'), + array('{/var,x}/here', '/value/1024/here'), + array('{;x,y}', ';x=1024;y=768'), + array('{;x,y,empty}', ';x=1024;y=768;empty'), + array('{?x,y}', '?x=1024&y=768'), + array('{?x,y,empty}', '?x=1024&y=768&empty='), + array('?fixed=yes{&x}', '?fixed=yes&x=1024'), + array('{&x,y,empty}', '&x=1024&y=768&empty='), + array('{var:3}', 'val'), + array('{var:30}', 'value'), + array('{list}', 'red,green,blue'), + array('{list*}', 'red,green,blue'), + array('{keys}', 'semi,%3B,dot,.,comma,%2C'), + array('{keys*}', 'semi=%3B,dot=.,comma=%2C'), + array('{+path:6}/here', '/foo/b/here'), + array('{+list}', 'red,green,blue'), + array('{+list*}', 'red,green,blue'), + array('{+keys}', 'semi,;,dot,.,comma,,'), + array('{+keys*}', 'semi=;,dot=.,comma=,'), + array('{#path:6}/here', '#/foo/b/here'), + array('{#list}', '#red,green,blue'), + array('{#list*}', '#red,green,blue'), + array('{#keys}', '#semi,;,dot,.,comma,,'), + array('{#keys*}', '#semi=;,dot=.,comma=,'), + array('X{.var:3}', 'X.val'), + array('X{.list}', 'X.red,green,blue'), + array('X{.list*}', 'X.red.green.blue'), + array('X{.keys}', 'X.semi,%3B,dot,.,comma,%2C'), + array('X{.keys*}', 'X.semi=%3B.dot=..comma=%2C'), + array('{/var:1,var}', '/v/value'), + array('{/list}', '/red,green,blue'), + array('{/list*}', '/red/green/blue'), + array('{/list*,path:4}', '/red/green/blue/%2Ffoo'), + array('{/keys}', '/semi,%3B,dot,.,comma,%2C'), + array('{/keys*}', '/semi=%3B/dot=./comma=%2C'), + array('{;hello:5}', ';hello=Hello'), + array('{;list}', ';list=red,green,blue'), + array('{;list*}', ';list=red;list=green;list=blue'), + array('{;keys}', ';keys=semi,%3B,dot,.,comma,%2C'), + array('{;keys*}', ';semi=%3B;dot=.;comma=%2C'), + array('{?var:3}', '?var=val'), + array('{?list}', '?list=red,green,blue'), + array('{?list*}', '?list=red&list=green&list=blue'), + array('{?keys}', '?keys=semi,%3B,dot,.,comma,%2C'), + array('{?keys*}', '?semi=%3B&dot=.&comma=%2C'), + array('{&var:3}', '&var=val'), + array('{&list}', '&list=red,green,blue'), + array('{&list*}', '&list=red&list=green&list=blue'), + array('{&keys}', '&keys=semi,%3B,dot,.,comma,%2C'), + array('{&keys*}', '&semi=%3B&dot=.&comma=%2C'), + array('{.null}', ''), + array('{.null,var}', '.value'), + array('X{.empty_keys*}', 'X'), + array('X{.empty_keys}', 'X'), + // Test that missing expansions are skipped + array('test{&missing*}', 'test'), + // Test that multiple expansions can be set + array('http://{var}/{var:2}{?keys*}', 'http://value/va?semi=%3B&dot=.&comma=%2C'), + // Test more complex query string stuff + array('http://www.test.com{+path}{?var,keys*}', 'http://www.test.com/foo/bar?var=value&semi=%3B&dot=.&comma=%2C') + )); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Parser/UriTemplate/PeclUriTemplateTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Parser/UriTemplate/PeclUriTemplateTest.php new file mode 100644 index 000000000..633c5d539 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Parser/UriTemplate/PeclUriTemplateTest.php @@ -0,0 +1,27 @@ +markTestSkipped('uri_template PECL extension must be installed to test PeclUriTemplate'); + } + } + + /** + * @dataProvider templateProvider + */ + public function testExpandsUriTemplates($template, $expansion, $params) + { + $uri = new PeclUriTemplate($template); + $this->assertEquals($expansion, $uri->expand($template, $params)); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Parser/UriTemplate/UriTemplateTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Parser/UriTemplate/UriTemplateTest.php new file mode 100644 index 000000000..5130d6f4b --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Parser/UriTemplate/UriTemplateTest.php @@ -0,0 +1,106 @@ +assertEquals($expansion, $uri->expand($template, $params)); + } + + public function expressionProvider() + { + return array( + array( + '{+var*}', array( + 'operator' => '+', + 'values' => array( + array('value' => 'var', 'modifier' => '*') + ) + ), + ), + array( + '{?keys,var,val}', array( + 'operator' => '?', + 'values' => array( + array('value' => 'keys', 'modifier' => ''), + array('value' => 'var', 'modifier' => ''), + array('value' => 'val', 'modifier' => '') + ) + ), + ), + array( + '{+x,hello,y}', array( + 'operator' => '+', + 'values' => array( + array('value' => 'x', 'modifier' => ''), + array('value' => 'hello', 'modifier' => ''), + array('value' => 'y', 'modifier' => '') + ) + ) + ) + ); + } + + /** + * @dataProvider expressionProvider + */ + public function testParsesExpressions($exp, $data) + { + $template = new UriTemplate($exp); + + // Access the config object + $class = new \ReflectionClass($template); + $method = $class->getMethod('parseExpression'); + $method->setAccessible(true); + + $exp = substr($exp, 1, -1); + $this->assertEquals($data, $method->invokeArgs($template, array($exp))); + } + + /** + * @ticket https://github.com/guzzle/guzzle/issues/90 + */ + public function testAllowsNestedArrayExpansion() + { + $template = new UriTemplate(); + + $result = $template->expand('http://example.com{+path}{/segments}{?query,data*,foo*}', array( + 'path' => '/foo/bar', + 'segments' => array('one', 'two'), + 'query' => 'test', + 'data' => array( + 'more' => array('fun', 'ice cream') + ), + 'foo' => array( + 'baz' => array( + 'bar' => 'fizz', + 'test' => 'buzz' + ), + 'bam' => 'boo' + ) + )); + + $this->assertEquals('http://example.com/foo/bar/one,two?query=test&more%5B0%5D=fun&more%5B1%5D=ice%20cream&baz%5Bbar%5D=fizz&baz%5Btest%5D=buzz&bam=boo', $result); + } + + /** + * @ticket https://github.com/guzzle/guzzle/issues/426 + */ + public function testSetRegex() + { + $template = new UriTemplate(); + $template->setRegex('/\<\$(.+)\>/'); + $this->assertSame('/foo', $template->expand('/<$a>', array('a' => 'foo'))); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Async/AsyncPluginTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Async/AsyncPluginTest.php new file mode 100644 index 000000000..16990a5a8 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Async/AsyncPluginTest.php @@ -0,0 +1,93 @@ +assertArrayHasKey('request.before_send', $events); + $this->assertArrayHasKey('request.exception', $events); + $this->assertArrayHasKey('curl.callback.progress', $events); + } + + public function testEnablesProgressCallbacks() + { + $p = new AsyncPlugin(); + $request = RequestFactory::getInstance()->create('PUT', 'http://www.example.com'); + $event = new Event(array( + 'request' => $request + )); + $p->onBeforeSend($event); + $this->assertEquals(true, $request->getCurlOptions()->get('progress')); + } + + public function testAddsTimesOutAfterSending() + { + $p = new AsyncPlugin(); + $request = RequestFactory::getInstance()->create('PUT', 'http://www.example.com'); + $handle = CurlHandle::factory($request); + $event = new Event(array( + 'request' => $request, + 'handle' => $handle->getHandle(), + 'uploaded' => 10, + 'upload_size' => 10, + 'downloaded' => 0 + )); + $p->onCurlProgress($event); + } + + public function testEnsuresRequestIsSet() + { + $p = new AsyncPlugin(); + $event = new Event(array( + 'uploaded' => 10, + 'upload_size' => 10, + 'downloaded' => 0 + )); + $p->onCurlProgress($event); + } + + public function testMasksCurlExceptions() + { + $p = new AsyncPlugin(); + $request = RequestFactory::getInstance()->create('PUT', 'http://www.example.com'); + $e = new CurlException('Error'); + $event = new Event(array( + 'request' => $request, + 'exception' => $e + )); + $p->onRequestTimeout($event); + $this->assertEquals(RequestInterface::STATE_COMPLETE, $request->getState()); + $this->assertEquals(200, $request->getResponse()->getStatusCode()); + $this->assertTrue($request->getResponse()->hasHeader('X-Guzzle-Async')); + } + + public function testEnsuresIntegration() + { + $this->getServer()->flush(); + $this->getServer()->enqueue("HTTP/1.1 204 FOO\r\nContent-Length: 4\r\n\r\ntest"); + $client = new Client($this->getServer()->getUrl()); + $request = $client->post('/', null, array( + 'foo' => 'bar' + )); + $request->getEventDispatcher()->addSubscriber(new AsyncPlugin()); + $request->send(); + $this->assertEquals('', $request->getResponse()->getBody(true)); + $this->assertTrue($request->getResponse()->hasHeader('X-Guzzle-Async')); + $received = $this->getServer()->getReceivedRequests(true); + $this->assertEquals('POST', $received[0]->getMethod()); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/AbstractBackoffStrategyTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/AbstractBackoffStrategyTest.php new file mode 100644 index 000000000..72af26308 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/AbstractBackoffStrategyTest.php @@ -0,0 +1,86 @@ +getMockBuilder('Guzzle\Plugin\Backoff\AbstractBackoffStrategy') + ->setMethods(array('getDelay', 'makesDecision')) + ->getMockForAbstractClass(); + } + + public function testReturnsZeroWhenNoNextAndGotNull() + { + $request = new Request('GET', 'http://www.foo.com'); + $mock = $this->getMockStrategy(); + $mock->expects($this->atLeastOnce())->method('getDelay')->will($this->returnValue(null)); + $this->assertEquals(0, $mock->getBackoffPeriod(0, $request)); + } + + public function testReturnsFalse() + { + $request = new Request('GET', 'http://www.foo.com'); + $mock = $this->getMockStrategy(); + $mock->expects($this->atLeastOnce())->method('getDelay')->will($this->returnValue(false)); + $this->assertEquals(false, $mock->getBackoffPeriod(0, $request)); + } + + public function testReturnsNextValueWhenNullOrTrue() + { + $request = new Request('GET', 'http://www.foo.com'); + $mock = $this->getMockStrategy(); + $mock->expects($this->atLeastOnce())->method('getDelay')->will($this->returnValue(null)); + $mock->expects($this->any())->method('makesDecision')->will($this->returnValue(false)); + + $mock2 = $this->getMockStrategy(); + $mock2->expects($this->atLeastOnce())->method('getDelay')->will($this->returnValue(10)); + $mock2->expects($this->atLeastOnce())->method('makesDecision')->will($this->returnValue(true)); + $mock->setNext($mock2); + + $this->assertEquals(10, $mock->getBackoffPeriod(0, $request)); + } + + public function testReturnsFalseWhenNullAndNoNext() + { + $request = new Request('GET', 'http://www.foo.com'); + $s = new TruncatedBackoffStrategy(2); + $this->assertFalse($s->getBackoffPeriod(0, $request)); + } + + public function testHasNext() + { + $a = new TruncatedBackoffStrategy(2); + $b = new TruncatedBackoffStrategy(2); + $a->setNext($b); + $this->assertSame($b, $a->getNext()); + } + + public function testSkipsOtherDecisionsInChainWhenOneReturnsTrue() + { + $a = new CallbackBackoffStrategy(function () { return null; }, true); + $b = new CallbackBackoffStrategy(function () { return true; }, true); + $c = new CallbackBackoffStrategy(function () { return null; }, true); + $d = new CallbackBackoffStrategy(function () { return 10; }, false); + $a->setNext($b); + $b->setNext($c); + $c->setNext($d); + $this->assertEquals(10, $a->getBackoffPeriod(2, new Request('GET', 'http://www.foo.com'))); + } + + public function testReturnsZeroWhenDecisionMakerReturnsTrueButNoFurtherStrategiesAreInTheChain() + { + $a = new CallbackBackoffStrategy(function () { return null; }, true); + $b = new CallbackBackoffStrategy(function () { return true; }, true); + $a->setNext($b); + $this->assertSame(0, $a->getBackoffPeriod(2, new Request('GET', 'http://www.foo.com'))); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/BackoffLoggerTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/BackoffLoggerTest.php new file mode 100644 index 000000000..a64dd826e --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/BackoffLoggerTest.php @@ -0,0 +1,110 @@ +message = ''; + } + + public function testHasEventList() + { + $this->assertEquals(1, count(BackoffLogger::getSubscribedEvents())); + } + + public function testLogsEvents() + { + list($logPlugin, $request, $response) = $this->getMocks(); + + $response = $this->getMockBuilder('Guzzle\Http\Message\Response') + ->setConstructorArgs(array(503)) + ->setMethods(array('getInfo')) + ->getMock(); + + $response->expects($this->any()) + ->method('getInfo') + ->will($this->returnValue(2)); + + $handle = $this->getMockHandle(); + + $event = new Event(array( + 'request' => $request, + 'response' => $response, + 'retries' => 1, + 'delay' => 3, + 'handle' => $handle + )); + + $logPlugin->onRequestRetry($event); + $this->assertContains( + '] PUT http://www.example.com - 503 Service Unavailable - Retries: 1, Delay: 3, Time: 2, 2, cURL: 30 Foo', + $this->message + ); + } + + public function testCanSetTemplate() + { + $l = new BackoffLogger(new ClosureLogAdapter(function () {})); + $l->setTemplate('foo'); + $t = $this->readAttribute($l, 'formatter'); + $this->assertEquals('foo', $this->readAttribute($t, 'template')); + } + + /** + * @return array + */ + protected function getMocks() + { + $that = $this; + $logger = new ClosureLogAdapter(function ($message) use ($that) { + $that->message .= $message . "\n"; + }); + $logPlugin = new BackoffLogger($logger); + $response = new Response(503); + $request = RequestFactory::getInstance()->create('PUT', 'http://www.example.com', array( + 'Content-Length' => 3, + 'Foo' => 'Bar' + )); + + return array($logPlugin, $request, $response); + } + + /** + * @return CurlHandle + */ + protected function getMockHandle() + { + $handle = $this->getMockBuilder('Guzzle\Http\Curl\CurlHandle') + ->disableOriginalConstructor() + ->setMethods(array('getError', 'getErrorNo', 'getInfo')) + ->getMock(); + + $handle->expects($this->once()) + ->method('getError') + ->will($this->returnValue('Foo')); + + $handle->expects($this->once()) + ->method('getErrorNo') + ->will($this->returnValue(30)); + + $handle->expects($this->any()) + ->method('getInfo') + ->will($this->returnValue(2)); + + return $handle; + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/BackoffPluginTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/BackoffPluginTest.php new file mode 100644 index 000000000..496e49eb2 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/BackoffPluginTest.php @@ -0,0 +1,297 @@ +retried = false; + } + + public static function getSubscribedEvents() + { + return array(BackoffPlugin::RETRY_EVENT => 'onRequestRetry'); + } + + public function onRequestRetry(Event $event) + { + $this->retried = $event; + } + + public function testHasEventList() + { + $this->assertEquals(1, count(BackoffPlugin::getAllEvents())); + } + + public function testCreatesDefaultExponentialBackoffPlugin() + { + $plugin = BackoffPlugin::getExponentialBackoff(3, array(204), array(10)); + $this->assertInstanceOf('Guzzle\Plugin\Backoff\BackoffPlugin', $plugin); + $strategy = $this->readAttribute($plugin, 'strategy'); + $this->assertInstanceOf('Guzzle\Plugin\Backoff\TruncatedBackoffStrategy', $strategy); + $this->assertEquals(3, $this->readAttribute($strategy, 'max')); + $strategy = $this->readAttribute($strategy, 'next'); + $this->assertInstanceOf('Guzzle\Plugin\Backoff\HttpBackoffStrategy', $strategy); + $this->assertEquals(array(204 => true), $this->readAttribute($strategy, 'errorCodes')); + $strategy = $this->readAttribute($strategy, 'next'); + $this->assertInstanceOf('Guzzle\Plugin\Backoff\CurlBackoffStrategy', $strategy); + $this->assertEquals(array(10 => true), $this->readAttribute($strategy, 'errorCodes')); + $strategy = $this->readAttribute($strategy, 'next'); + $this->assertInstanceOf('Guzzle\Plugin\Backoff\ExponentialBackoffStrategy', $strategy); + } + + public function testDoesNotRetryUnlessStrategyReturnsNumber() + { + $request = new Request('GET', 'http://www.example.com'); + $request->setState('transfer'); + + $mock = $this->getMockBuilder('Guzzle\Plugin\Backoff\BackoffStrategyInterface') + ->setMethods(array('getBackoffPeriod')) + ->getMockForAbstractClass(); + + $mock->expects($this->once()) + ->method('getBackoffPeriod') + ->will($this->returnValue(false)); + + $plugin = new BackoffPlugin($mock); + $plugin->addSubscriber($this); + $plugin->onRequestSent(new Event(array('request' => $request))); + $this->assertFalse($this->retried); + } + + public function testUpdatesRequestForRetry() + { + $request = new Request('GET', 'http://www.example.com'); + $request->setState('transfer'); + $response = new Response(500); + $handle = $this->getMockBuilder('Guzzle\Http\Curl\CurlHandle')->disableOriginalConstructor()->getMock(); + $e = new CurlException(); + $e->setCurlHandle($handle); + + $plugin = new BackoffPlugin(new ConstantBackoffStrategy(10)); + $plugin->addSubscriber($this); + + $event = new Event(array( + 'request' => $request, + 'response' => $response, + 'exception' => $e + )); + + $plugin->onRequestSent($event); + $this->assertEquals(array( + 'request' => $request, + 'response' => $response, + 'handle' => $handle, + 'retries' => 1, + 'delay' => 10 + ), $this->readAttribute($this->retried, 'context')); + + $plugin->onRequestSent($event); + $this->assertEquals(array( + 'request' => $request, + 'response' => $response, + 'handle' => $handle, + 'retries' => 2, + 'delay' => 10 + ), $this->readAttribute($this->retried, 'context')); + } + + public function testDoesNothingWhenNotRetryingAndPollingRequest() + { + $request = new Request('GET', 'http://www.foo.com'); + $plugin = new BackoffPlugin(new ConstantBackoffStrategy(10)); + $plugin->onRequestPoll(new Event(array('request' => $request))); + } + + public function testRetriesRequests() + { + // Create a script to return several 500 and 503 response codes + $this->getServer()->flush(); + $this->getServer()->enqueue(array( + "HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\n\r\n", + "HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\n\r\n", + "HTTP/1.1 200 OK\r\nContent-Length: 4\r\n\r\ndata" + )); + + $plugin = new BackoffPlugin( + new TruncatedBackoffStrategy(3, + new HttpBackoffStrategy(null, + new CurlBackoffStrategy(null, + new ConstantBackoffStrategy(0.05) + ) + ) + ) + ); + + $client = new Client($this->getServer()->getUrl()); + $client->getEventDispatcher()->addSubscriber($plugin); + $request = $client->get(); + $request->send(); + + // Make sure it eventually completed successfully + $this->assertEquals(200, $request->getResponse()->getStatusCode()); + $this->assertEquals('data', $request->getResponse()->getBody(true)); + + // Check that three requests were made to retry this request + $this->assertEquals(3, count($this->getServer()->getReceivedRequests(false))); + $this->assertEquals(2, $request->getParams()->get(BackoffPlugin::RETRY_PARAM)); + } + + /** + * @expectedException \Guzzle\Http\Exception\ServerErrorResponseException + */ + public function testFailsOnTruncation() + { + $this->getServer()->flush(); + $this->getServer()->enqueue(array( + "HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\n\r\n", + "HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\n\r\n" + )); + + $plugin = new BackoffPlugin( + new TruncatedBackoffStrategy(2, + new HttpBackoffStrategy(null, + new ConstantBackoffStrategy(0.05) + ) + ) + ); + + $client = new Client($this->getServer()->getUrl()); + $client->addSubscriber($plugin); + $client->get()->send(); + } + + public function testRetriesRequestsWhenInParallel() + { + // Create a script to return several 500 and 503 response codes + $this->getServer()->flush(); + $this->getServer()->enqueue(array( + "HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\n\r\n", + "HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\n\r\n", + "HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\n\r\n", + "HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\n\r\n", + "HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\n\r\n", + "HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\n\r\n", + "HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\n\r\n", + "HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\n\r\n", + "HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\n\r\n", + "HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\n\r\n", + "HTTP/1.1 200 OK\r\nContent-Length: 4\r\n\r\ndata", + "HTTP/1.1 200 OK\r\nContent-Length: 4\r\n\r\ndata", + "HTTP/1.1 200 OK\r\nContent-Length: 4\r\n\r\ndata", + "HTTP/1.1 200 OK\r\nContent-Length: 4\r\n\r\ndata", + "HTTP/1.1 200 OK\r\nContent-Length: 4\r\n\r\ndata" + )); + + $plugin = new BackoffPlugin( + new HttpBackoffStrategy(null, + new TruncatedBackoffStrategy(3, + new CurlBackoffStrategy(null, + new ConstantBackoffStrategy(0.1) + ) + ) + ) + ); + $client = new Client($this->getServer()->getUrl()); + $client->getEventDispatcher()->addSubscriber($plugin); + $requests = array(); + for ($i = 0; $i < 5; $i++) { + $requests[] = $client->get(); + } + $client->send($requests); + + $this->assertEquals(15, count($this->getServer()->getReceivedRequests(false))); + } + + /** + * @covers Guzzle\Plugin\Backoff\BackoffPlugin + * @covers Guzzle\Http\Curl\CurlMulti + */ + public function testRetriesPooledRequestsUsingDelayAndPollingEvent() + { + $this->getServer()->flush(); + $this->getServer()->enqueue(array( + "HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\n\r\n", + "HTTP/1.1 200 OK\r\nContent-Length: 4\r\n\r\ndata" + )); + // Need to sleep for some time ensure that the polling works correctly in the observer + $plugin = new BackoffPlugin(new HttpBackoffStrategy(null, + new TruncatedBackoffStrategy(1, + new ConstantBackoffStrategy(0.5)))); + + $client = new Client($this->getServer()->getUrl()); + $client->getEventDispatcher()->addSubscriber($plugin); + $request = $client->get(); + $request->send(); + // Make sure it eventually completed successfully + $this->assertEquals('data', $request->getResponse()->getBody(true)); + // Check that two requests were made to retry this request + $this->assertEquals(2, count($this->getServer()->getReceivedRequests(false))); + } + + public function testSeeksToBeginningOfRequestBodyWhenRetrying() + { + // Create a request with a body + $request = new EntityEnclosingRequest('PUT', 'http://www.example.com'); + $request->setBody('abc'); + // Set the retry time to be something that will be retried always + $request->getParams()->set(BackoffPlugin::DELAY_PARAM, 2); + // Seek to the end of the stream + $request->getBody()->seek(3); + $this->assertEquals('', $request->getBody()->read(1)); + // Create a plugin that does not delay when retrying + $plugin = new BackoffPlugin(new ConstantBackoffStrategy(0)); + $plugin->onRequestPoll($this->getMockEvent($request)); + // Ensure that the stream was seeked to 0 + $this->assertEquals('a', $request->getBody()->read(1)); + } + + public function testDoesNotSeekOnRequestsWithNoBodyWhenRetrying() + { + // Create a request with a body + $request = new EntityEnclosingRequest('PUT', 'http://www.example.com'); + $request->getParams()->set(BackoffPlugin::DELAY_PARAM, 2); + $plugin = new BackoffPlugin(new ConstantBackoffStrategy(0)); + $plugin->onRequestPoll($this->getMockEvent($request)); + } + + protected function getMockEvent(RequestInterface $request) + { + // Create a mock curl multi object + $multi = $this->getMockBuilder('Guzzle\Http\Curl\CurlMulti') + ->setMethods(array('remove', 'add')) + ->getMock(); + + // Create an event that is expected for the Poll event + $event = new Event(array( + 'request' => $request, + 'curl_multi' => $multi + )); + $event->setName(CurlMultiInterface::POLLING_REQUEST); + + return $event; + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/CallbackBackoffStrategyTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/CallbackBackoffStrategyTest.php new file mode 100644 index 000000000..c0ce10d5c --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/CallbackBackoffStrategyTest.php @@ -0,0 +1,31 @@ +getMock('Guzzle\Http\Message\Request', array(), array(), '', false); + $strategy = new CallbackBackoffStrategy(function () { return 10; }, true); + $this->assertTrue($strategy->makesDecision()); + $this->assertEquals(10, $strategy->getBackoffPeriod(0, $request)); + // Ensure it chains correctly when null is returned + $strategy = new CallbackBackoffStrategy(function () { return null; }, false); + $this->assertFalse($strategy->makesDecision()); + $this->assertFalse($strategy->getBackoffPeriod(0, $request)); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/ConstantBackoffStrategyTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/ConstantBackoffStrategyTest.php new file mode 100644 index 000000000..703eb4a22 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/ConstantBackoffStrategyTest.php @@ -0,0 +1,20 @@ +assertFalse($strategy->makesDecision()); + $request = $this->getMock('Guzzle\Http\Message\Request', array(), array(), '', false); + $this->assertEquals(3.5, $strategy->getBackoffPeriod(0, $request)); + $this->assertEquals(3.5, $strategy->getBackoffPeriod(1, $request)); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/CurlBackoffStrategyTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/CurlBackoffStrategyTest.php new file mode 100644 index 000000000..0a5c3e28d --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/CurlBackoffStrategyTest.php @@ -0,0 +1,36 @@ +assertNotEmpty(CurlBackoffStrategy::getDefaultFailureCodes()); + $strategy = new CurlBackoffStrategy(); + $this->assertTrue($strategy->makesDecision()); + $request = $this->getMock('Guzzle\Http\Message\Request', array(), array(), '', false); + $e = new CurlException(); + $e->setError('foo', CURLE_BAD_CALLING_ORDER); + $this->assertEquals(false, $strategy->getBackoffPeriod(0, $request, null, $e)); + + foreach (CurlBackoffStrategy::getDefaultFailureCodes() as $code) { + $this->assertEquals(0, $strategy->getBackoffPeriod(0, $request, null, $e->setError('foo', $code))); + } + } + + public function testIgnoresNonErrors() + { + $strategy = new CurlBackoffStrategy(); + $request = $this->getMock('Guzzle\Http\Message\Request', array(), array(), '', false); + $this->assertEquals(false, $strategy->getBackoffPeriod(0, $request, new Response(200))); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/ExponentialBackoffStrategyTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/ExponentialBackoffStrategyTest.php new file mode 100644 index 000000000..09965bcbf --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/ExponentialBackoffStrategyTest.php @@ -0,0 +1,23 @@ +assertFalse($strategy->makesDecision()); + $request = $this->getMock('Guzzle\Http\Message\Request', array(), array(), '', false); + $this->assertEquals(1, $strategy->getBackoffPeriod(0, $request)); + $this->assertEquals(2, $strategy->getBackoffPeriod(1, $request)); + $this->assertEquals(4, $strategy->getBackoffPeriod(2, $request)); + $this->assertEquals(8, $strategy->getBackoffPeriod(3, $request)); + $this->assertEquals(16, $strategy->getBackoffPeriod(4, $request)); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/HttpBackoffStrategyTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/HttpBackoffStrategyTest.php new file mode 100644 index 000000000..ae68a4eb8 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/HttpBackoffStrategyTest.php @@ -0,0 +1,47 @@ +assertNotEmpty(HttpBackoffStrategy::getDefaultFailureCodes()); + $strategy = new HttpBackoffStrategy(); + $this->assertTrue($strategy->makesDecision()); + $request = $this->getMock('Guzzle\Http\Message\Request', array(), array(), '', false); + + $response = new Response(200); + $this->assertEquals(false, $strategy->getBackoffPeriod(0, $request, $response)); + $response->setStatus(400); + $this->assertEquals(false, $strategy->getBackoffPeriod(0, $request, $response)); + + foreach (HttpBackoffStrategy::getDefaultFailureCodes() as $code) { + $this->assertEquals(0, $strategy->getBackoffPeriod(0, $request, $response->setStatus($code))); + } + } + + public function testAllowsCustomCodes() + { + $strategy = new HttpBackoffStrategy(array(204)); + $request = $this->getMock('Guzzle\Http\Message\Request', array(), array(), '', false); + $response = new Response(204); + $this->assertEquals(0, $strategy->getBackoffPeriod(0, $request, $response)); + $response->setStatus(500); + $this->assertEquals(false, $strategy->getBackoffPeriod(0, $request, $response)); + } + + public function testIgnoresNonErrors() + { + $strategy = new HttpBackoffStrategy(); + $request = $this->getMock('Guzzle\Http\Message\Request', array(), array(), '', false); + $this->assertEquals(false, $strategy->getBackoffPeriod(0, $request)); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/LinearBackoffStrategyTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/LinearBackoffStrategyTest.php new file mode 100644 index 000000000..b4ce8e4af --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/LinearBackoffStrategyTest.php @@ -0,0 +1,21 @@ +assertFalse($strategy->makesDecision()); + $request = $this->getMock('Guzzle\Http\Message\Request', array(), array(), '', false); + $this->assertEquals(0, $strategy->getBackoffPeriod(0, $request)); + $this->assertEquals(5, $strategy->getBackoffPeriod(1, $request)); + $this->assertEquals(10, $strategy->getBackoffPeriod(2, $request)); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/ReasonPhraseBackoffStrategyTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/ReasonPhraseBackoffStrategyTest.php new file mode 100644 index 000000000..dea5a6878 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/ReasonPhraseBackoffStrategyTest.php @@ -0,0 +1,32 @@ +assertEmpty(ReasonPhraseBackoffStrategy::getDefaultFailureCodes()); + $strategy = new ReasonPhraseBackoffStrategy(array('Foo', 'Internal Server Error')); + $this->assertTrue($strategy->makesDecision()); + $request = $this->getMock('Guzzle\Http\Message\Request', array(), array(), '', false); + $response = new Response(200); + $this->assertEquals(false, $strategy->getBackoffPeriod(0, $request, $response)); + $response->setStatus(200, 'Foo'); + $this->assertEquals(0, $strategy->getBackoffPeriod(0, $request, $response)); + } + + public function testIgnoresNonErrors() + { + $strategy = new ReasonPhraseBackoffStrategy(); + $request = $this->getMock('Guzzle\Http\Message\Request', array(), array(), '', false); + $this->assertEquals(false, $strategy->getBackoffPeriod(0, $request)); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/TruncatedBackoffStrategyTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/TruncatedBackoffStrategyTest.php new file mode 100644 index 000000000..5590dfb1c --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Backoff/TruncatedBackoffStrategyTest.php @@ -0,0 +1,30 @@ +assertTrue($strategy->makesDecision()); + $request = $this->getMock('Guzzle\Http\Message\Request', array(), array(), '', false); + $this->assertFalse($strategy->getBackoffPeriod(0, $request)); + $this->assertFalse($strategy->getBackoffPeriod(1, $request)); + $this->assertFalse($strategy->getBackoffPeriod(2, $request)); + + $response = new Response(500); + $strategy->setNext(new HttpBackoffStrategy(null, new ConstantBackoffStrategy(10))); + $this->assertEquals(10, $strategy->getBackoffPeriod(0, $request, $response)); + $this->assertEquals(10, $strategy->getBackoffPeriod(1, $request, $response)); + $this->assertFalse($strategy->getBackoffPeriod(2, $request, $response)); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cache/CachePluginTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cache/CachePluginTest.php new file mode 100644 index 000000000..69da60a93 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cache/CachePluginTest.php @@ -0,0 +1,441 @@ +assertInstanceOf('Guzzle\Plugin\Cache\CacheStorageInterface', $this->readAttribute($plugin, 'storage')); + } + + public function testAddsDefaultCollaborators() + { + $this->assertNotEmpty(CachePlugin::getSubscribedEvents()); + $plugin = new CachePlugin(array( + 'storage' => $this->getMockBuilder('Guzzle\Plugin\Cache\CacheStorageInterface')->getMockForAbstractClass() + )); + $this->assertInstanceOf('Guzzle\Plugin\Cache\CacheStorageInterface', $this->readAttribute($plugin, 'storage')); + $this->assertInstanceOf( + 'Guzzle\Plugin\Cache\CanCacheStrategyInterface', + $this->readAttribute($plugin, 'canCache') + ); + $this->assertInstanceOf( + 'Guzzle\Plugin\Cache\RevalidationInterface', + $this->readAttribute($plugin, 'revalidation') + ); + } + + public function testAddsCallbackCollaborators() + { + $this->assertNotEmpty(CachePlugin::getSubscribedEvents()); + $plugin = new CachePlugin(array('can_cache' => function () {})); + $this->assertInstanceOf( + 'Guzzle\Plugin\Cache\CallbackCanCacheStrategy', + $this->readAttribute($plugin, 'canCache') + ); + } + + public function testCanPassCacheAsOnlyArgumentToConstructor() + { + $p = new CachePlugin(new DoctrineCacheAdapter(new ArrayCache())); + $p = new CachePlugin(new DefaultCacheStorage(new DoctrineCacheAdapter(new ArrayCache()))); + } + + public function testUsesCreatedCacheStorage() + { + $plugin = new CachePlugin(array( + 'adapter' => $this->getMockBuilder('Guzzle\Cache\CacheAdapterInterface')->getMockForAbstractClass() + )); + $this->assertInstanceOf('Guzzle\Plugin\Cache\CacheStorageInterface', $this->readAttribute($plugin, 'storage')); + } + + public function testUsesProvidedOptions() + { + $can = $this->getMockBuilder('Guzzle\Plugin\Cache\CanCacheStrategyInterface')->getMockForAbstractClass(); + $revalidate = $this->getMockBuilder('Guzzle\Plugin\Cache\RevalidationInterface')->getMockForAbstractClass(); + $plugin = new CachePlugin(array( + 'storage' => $this->getMockBuilder('Guzzle\Plugin\Cache\CacheStorageInterface')->getMockForAbstractClass(), + 'can_cache' => $can, + 'revalidation' => $revalidate + )); + $this->assertSame($can, $this->readAttribute($plugin, 'canCache')); + $this->assertSame($revalidate, $this->readAttribute($plugin, 'revalidation')); + } + + public function satisfyProvider() + { + $req1 = new Request('GET', 'http://foo.com', array('Cache-Control' => 'no-cache')); + + return array( + // The response is too old to satisfy the request + array(new Request('GET', 'http://foo.com', array('Cache-Control' => 'max-age=20')), new Response(200, array('Age' => 100)), false, false), + // The response cannot satisfy the request because it is stale + array(new Request('GET', 'http://foo.com'), new Response(200, array('Cache-Control' => 'max-age=10', 'Age' => 100)), false, false), + // Allows the expired response to satisfy the request because of the max-stale + array(new Request('GET', 'http://foo.com', array('Cache-Control' => 'max-stale=15')), new Response(200, array('Cache-Control' => 'max-age=90', 'Age' => 100)), true, false), + // Max stale is > than the allowed staleness + array(new Request('GET', 'http://foo.com', array('Cache-Control' => 'max-stale=5')), new Response(200, array('Cache-Control' => 'max-age=90', 'Age' => 100)), false, false), + // Performs cache revalidation + array($req1, new Response(200), true, true), + // Performs revalidation due to ETag on the response and no cache-control on the request + array(new Request('GET', 'http://foo.com'), new Response(200, array( + 'ETag' => 'ABC', + 'Expires' => date('c', strtotime('+1 year')) + )), true, true), + ); + } + + /** + * @dataProvider satisfyProvider + */ + public function testChecksIfResponseCanSatisfyRequest($request, $response, $can, $revalidates) + { + $didRevalidate = false; + $storage = $this->getMockBuilder('Guzzle\Plugin\Cache\CacheStorageInterface')->getMockForAbstractClass(); + $revalidate = $this->getMockBuilder('Guzzle\Plugin\Cache\DefaultRevalidation') + ->setMethods(array('revalidate')) + ->setConstructorArgs(array($storage)) + ->getMockForAbstractClass(); + + $revalidate->expects($this->any()) + ->method('revalidate') + ->will($this->returnCallback(function () use (&$didRevalidate) { + $didRevalidate = true; + return true; + })); + + $plugin = new CachePlugin(array( + 'storage' => $storage, + 'revalidation' => $revalidate + )); + + $this->assertEquals($can, $plugin->canResponseSatisfyRequest($request, $response)); + $this->assertEquals($didRevalidate, $revalidates); + } + + public function satisfyFailedProvider() + { + return array( + // Neither has stale-if-error + array(new Request('GET', 'http://foo.com', array()), new Response(200, array('Age' => 100)), false), + // Request has stale-if-error + array(new Request('GET', 'http://foo.com', array('Cache-Control' => 'stale-if-error')), new Response(200, array('Age' => 100, 'Cache-Control' => 'max-age=50')), true), + // Request has valid stale-if-error + array(new Request('GET', 'http://foo.com', array('Cache-Control' => 'stale-if-error=50')), new Response(200, array('Age' => 100, 'Cache-Control' => 'max-age=50')), true), + // Request has expired stale-if-error + array(new Request('GET', 'http://foo.com', array('Cache-Control' => 'stale-if-error=20')), new Response(200, array('Age' => 100, 'Cache-Control' => 'max-age=50')), false), + // Response has permanent stale-if-error + array(new Request('GET', 'http://foo.com', array()), new Response(200, array('Age' => 100, 'Cache-Control' => 'max-age=50, stale-if-error', )), true), + // Response has valid stale-if-error + array(new Request('GET', 'http://foo.com', array()), new Response(200, array('Age' => 100, 'Cache-Control' => 'max-age=50, stale-if-error=50')), true), + // Response has expired stale-if-error + array(new Request('GET', 'http://foo.com', array()), new Response(200, array('Age' => 100, 'Cache-Control' => 'max-age=50, stale-if-error=20')), false), + // Request has valid stale-if-error but response does not + array(new Request('GET', 'http://foo.com', array('Cache-Control' => 'stale-if-error=50')), new Response(200, array('Age' => 100, 'Cache-Control' => 'max-age=50, stale-if-error=20')), false), + // Response has valid stale-if-error but request does not + array(new Request('GET', 'http://foo.com', array('Cache-Control' => 'stale-if-error=20')), new Response(200, array('Age' => 100, 'Cache-Control' => 'max-age=50, stale-if-error=50')), false), + ); + } + + /** + * @dataProvider satisfyFailedProvider + */ + public function testChecksIfResponseCanSatisfyFailedRequest($request, $response, $can) + { + $plugin = new CachePlugin(); + + $this->assertEquals($can, $plugin->canResponseSatisfyFailedRequest($request, $response)); + } + + public function testDoesNothingWhenRequestIsNotCacheable() + { + $storage = $this->getMockBuilder('Guzzle\Plugin\Cache\CacheStorageInterface') + ->setMethods(array('fetch')) + ->getMockForAbstractClass(); + $storage->expects($this->never())->method('fetch'); + + $plugin = new CachePlugin(array( + 'storage' => $storage, + 'can_cache' => new CallbackCanCacheStrategy(function () { return false; }) + )); + + $plugin->onRequestBeforeSend(new Event(array( + 'request' => new Request('GET', 'http://foo.com') + ))); + } + + public function satisfiableProvider() + { + $date = new \DateTime('-10 seconds'); + + return array( + // Fresh response + array(new Response(200, array(), 'foo')), + // Stale response + array(new Response(200, array('Date' => $date->format('c'), 'Cache-Control' => 'max-age=5'), 'foo')) + ); + } + + /** + * @dataProvider satisfiableProvider + */ + public function testInjectsSatisfiableResponses($response) + { + $storage = $this->getMockBuilder('Guzzle\Plugin\Cache\CacheStorageInterface') + ->setMethods(array('fetch')) + ->getMockForAbstractClass(); + + $storage->expects($this->once())->method('fetch')->will($this->returnValue($response)); + $plugin = new CachePlugin(array('storage' => $storage)); + $request = new Request('GET', 'http://foo.com', array('Cache-Control' => 'max-stale')); + $plugin->onRequestBeforeSend(new Event(array('request' => $request))); + $plugin->onRequestSent(new Event(array('request' => $request, 'response' => $request->getResponse()))); + $this->assertEquals($response->getStatusCode(), $request->getResponse()->getStatusCode()); + $this->assertEquals((string) $response->getBody(), (string) $request->getResponse()->getBody()); + $this->assertTrue($request->getResponse()->hasHeader('Age')); + if ($request->getResponse()->isFresh() === false) { + $this->assertContains('110', (string) $request->getResponse()->getHeader('Warning')); + } + $this->assertSame( + sprintf('%s GuzzleCache/%s', $request->getProtocolVersion(), Version::VERSION), + (string) $request->getHeader('Via') + ); + $this->assertSame( + sprintf('%s GuzzleCache/%s',$request->getProtocolVersion(), Version::VERSION), + (string) $request->getResponse()->getHeader('Via') + ); + $this->assertTrue($request->getParams()->get('cache.lookup')); + $this->assertTrue($request->getParams()->get('cache.hit')); + $this->assertTrue($request->getResponse()->hasHeader('X-Cache-Lookup')); + $this->assertTrue($request->getResponse()->hasHeader('X-Cache')); + $this->assertEquals('HIT from GuzzleCache', (string) $request->getResponse()->getHeader('X-Cache')); + $this->assertEquals('HIT from GuzzleCache', (string) $request->getResponse()->getHeader('X-Cache-Lookup')); + } + + public function satisfiableOnErrorProvider() + { + $date = new \DateTime('-10 seconds'); + return array( + array( + new Response(200, array( + 'Date' => $date->format('c'), + 'Cache-Control' => 'max-age=5, stale-if-error' + ), 'foo'), + ) + ); + } + + /** + * @dataProvider satisfiableOnErrorProvider + */ + public function testInjectsSatisfiableResponsesOnError($cacheResponse) + { + $storage = $this->getMockBuilder('Guzzle\Plugin\Cache\CacheStorageInterface') + ->setMethods(array('fetch')) + ->getMockForAbstractClass(); + $storage->expects($this->exactly(2))->method('fetch')->will($this->returnValue($cacheResponse)); + $plugin = new CachePlugin(array('storage' => $storage)); + $request = new Request('GET', 'http://foo.com', array('Cache-Control' => 'max-stale')); + $plugin->onRequestBeforeSend(new Event(array('request' => $request))); + $plugin->onRequestError( + $event = new Event(array( + 'request' => $request, + 'response' => $request->getResponse(), + )) + ); + $response = $event['response']; + $this->assertEquals($cacheResponse->getStatusCode(), $response->getStatusCode()); + $this->assertEquals((string) $cacheResponse->getBody(), (string) $response->getBody()); + $this->assertTrue($response->hasHeader('Age')); + if ($response->isFresh() === false) { + $this->assertContains('110', (string) $response->getHeader('Warning')); + } + $this->assertSame(sprintf('%s GuzzleCache/%s', $request->getProtocolVersion(), Version::VERSION), (string) $request->getHeader('Via')); + $this->assertSame(sprintf('%s GuzzleCache/%s', $request->getProtocolVersion(), Version::VERSION), (string) $response->getHeader('Via')); + $this->assertTrue($request->getParams()->get('cache.lookup')); + $this->assertSame('error', $request->getParams()->get('cache.hit')); + $this->assertTrue($response->hasHeader('X-Cache-Lookup')); + $this->assertTrue($response->hasHeader('X-Cache')); + $this->assertEquals('HIT from GuzzleCache', (string) $response->getHeader('X-Cache-Lookup')); + $this->assertEquals('HIT_ERROR from GuzzleCache', (string) $response->getHeader('X-Cache')); + } + + /** + * @dataProvider satisfiableOnErrorProvider + */ + public function testInjectsSatisfiableResponsesOnException($cacheResponse) + { + $storage = $this->getMockBuilder('Guzzle\Plugin\Cache\CacheStorageInterface') + ->setMethods(array('fetch')) + ->getMockForAbstractClass(); + $storage->expects($this->exactly(2))->method('fetch')->will($this->returnValue($cacheResponse)); + $plugin = new CachePlugin(array('storage' => $storage)); + $request = new Request('GET', 'http://foo.com', array('Cache-Control' => 'max-stale')); + $plugin->onRequestBeforeSend(new Event(array( + 'request' => $request + ))); + $plugin->onRequestException( + new Event(array( + 'request' => $request, + 'response' => $request->getResponse(), + 'exception' => $this->getMock('Guzzle\Http\Exception\CurlException'), + )) + ); + $plugin->onRequestSent( + new Event(array( + 'request' => $request, + 'response' => $response = $request->getResponse(), + )) + ); + $this->assertEquals($cacheResponse->getStatusCode(), $response->getStatusCode()); + $this->assertEquals((string) $cacheResponse->getBody(), (string) $response->getBody()); + $this->assertTrue($response->hasHeader('Age')); + if ($response->isFresh() === false) { + $this->assertContains('110', (string) $response->getHeader('Warning')); + } + $this->assertSame(sprintf('%s GuzzleCache/%s', $request->getProtocolVersion(), Version::VERSION), (string) $request->getHeader('Via')); + $this->assertSame(sprintf('%s GuzzleCache/%s', $request->getProtocolVersion(), Version::VERSION), (string) $response->getHeader('Via')); + $this->assertTrue($request->getParams()->get('cache.lookup')); + $this->assertSame('error', $request->getParams()->get('cache.hit')); + $this->assertTrue($response->hasHeader('X-Cache-Lookup')); + $this->assertTrue($response->hasHeader('X-Cache')); + $this->assertEquals('HIT from GuzzleCache', (string) $response->getHeader('X-Cache-Lookup')); + $this->assertEquals('HIT_ERROR from GuzzleCache', (string) $response->getHeader('X-Cache')); + } + + public function unsatisfiableOnErrorProvider() + { + $date = new \DateTime('-10 seconds'); + + return array( + // no-store on request + array( + false, + array('Cache-Control' => 'no-store'), + new Response(200, array('Date' => $date->format('D, d M Y H:i:s T'), 'Cache-Control' => 'max-age=5, stale-if-error'), 'foo'), + ), + // request expired + array( + true, + array('Cache-Control' => 'stale-if-error=4'), + new Response(200, array('Date' => $date->format('D, d M Y H:i:s T'), 'Cache-Control' => 'max-age=5, stale-if-error'), 'foo'), + ), + // response expired + array( + true, + array('Cache-Control' => 'stale-if-error'), + new Response(200, array('Date' => $date->format('D, d M Y H:i:s T'), 'Cache-Control' => 'max-age=5, stale-if-error=4'), 'foo'), + ), + ); + } + + /** + * @dataProvider unsatisfiableOnErrorProvider + */ + public function testDoesNotInjectUnsatisfiableResponsesOnError($requestCanCache, $requestHeaders, $cacheResponse) + { + $storage = $this->getMockBuilder('Guzzle\Plugin\Cache\CacheStorageInterface') + ->setMethods(array('fetch')) + ->getMockForAbstractClass(); + $storage->expects($this->exactly($requestCanCache ? 2 : 0))->method('fetch')->will($this->returnValue($cacheResponse)); + $plugin = new CachePlugin(array('storage' => $storage)); + $request = new Request('GET', 'http://foo.com', $requestHeaders); + $plugin->onRequestBeforeSend(new Event(array( + 'request' => $request + ))); + $plugin->onRequestError( + $event = new Event(array( + 'request' => $request, + 'response' => $response = $request->getResponse(), + )) + ); + + $this->assertSame($response, $event['response']); + } + + /** + * @dataProvider unsatisfiableOnErrorProvider + */ + public function testDoesNotInjectUnsatisfiableResponsesOnException($requestCanCache, $requestHeaders, $responseParts) + { + $storage = $this->getMockBuilder('Guzzle\Plugin\Cache\CacheStorageInterface') + ->setMethods(array('fetch')) + ->getMockForAbstractClass(); + $storage->expects($this->exactly($requestCanCache ? 2 : 0))->method('fetch')->will($this->returnValue($responseParts)); + $plugin = new CachePlugin(array('storage' => $storage)); + $request = new Request('GET', 'http://foo.com', $requestHeaders); + $plugin->onRequestBeforeSend(new Event(array( + 'request' => $request + ))); + $plugin->onRequestException( + $event = new Event(array( + 'request' => $request, + 'response' => $response = $request->getResponse(), + 'exception' => $this->getMock('Guzzle\Http\Exception\CurlException'), + )) + ); + + $this->assertSame($response, $request->getResponse()); + } + + public function testCachesResponsesWhenCacheable() + { + $cache = new ArrayCache(); + $plugin = new CachePlugin($cache); + + $request = new Request('GET', 'http://foo.com'); + $response = new Response(200, array(), 'Foo'); + $plugin->onRequestBeforeSend(new Event(array( + 'request' => $request + ))); + $plugin->onRequestSent(new Event(array( + 'request' => $request, + 'response' => $response + ))); + $data = $this->readAttribute($cache, 'data'); + $this->assertNotEmpty($data); + } + + public function testPurgesRequests() + { + $storage = $this->getMockBuilder('Guzzle\Plugin\Cache\CacheStorageInterface') + ->setMethods(array('purge')) + ->getMockForAbstractClass(); + $storage->expects($this->atLeastOnce())->method('purge'); + $plugin = new CachePlugin(array('storage' => $storage)); + $request = new Request('GET', 'http://foo.com', array('X-Foo' => 'Bar')); + $plugin->purge($request); + } + + public function testAutoPurgesRequests() + { + $storage = $this->getMockBuilder('Guzzle\Plugin\Cache\CacheStorageInterface') + ->setMethods(array('purge')) + ->getMockForAbstractClass(); + $storage->expects($this->atLeastOnce())->method('purge'); + $plugin = new CachePlugin(array('storage' => $storage, 'auto_purge' => true)); + $client = new Client(); + $request = $client->put('http://foo.com', array('X-Foo' => 'Bar')); + $request->addSubscriber($plugin); + $request->setResponse(new Response(200), true); + $request->send(); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cache/CallbackCanCacheStrategyTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cache/CallbackCanCacheStrategyTest.php new file mode 100644 index 000000000..f3d9bafe2 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cache/CallbackCanCacheStrategyTest.php @@ -0,0 +1,72 @@ +assertTrue($c->canCacheRequest(new Request('DELETE', 'http://www.foo.com'))); + } + + /** + * The following is a bit of an integration test to ensure that the CachePlugin honors a + * custom can cache strategy. + */ + public function testIntegrationWithCachePlugin() + { + $c = new CallbackCanCacheStrategy( + function ($request) { return true; }, + function ($response) { return true; } + ); + + // Make a request and response that have no business being cached + $request = new Request('DELETE', 'http://www.foo.com'); + $response = Response::fromMessage( + "HTTP/1.1 200 OK\r\n" + . "Expires: Mon, 26 Jul 1997 05:00:00 GMT\r\n" + . "Last-Modified: Wed, 09 Jan 2013 08:48:53 GMT\r\n" + . "Content-Length: 2\r\n" + . "Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0\r\n\r\n" + . "hi" + ); + + $this->assertTrue($c->canCacheRequest($request)); + $this->assertTrue($c->canCacheResponse($response)); + + $s = $this->getMockBuilder('Guzzle\Plugin\Cache\DefaultCacheStorage') + ->setConstructorArgs(array(new DoctrineCacheAdapter(new ArrayCache()))) + ->setMethods(array('fetch')) + ->getMockForAbstractClass(); + + $s->expects($this->once()) + ->method('fetch') + ->will($this->returnValue($response)); + + $plugin = new CachePlugin(array('can_cache' => $c, 'storage' => $s)); + $plugin->onRequestBeforeSend(new Event(array('request' => $request))); + + $this->assertEquals(200, $request->getResponse()->getStatusCode()); + $this->assertEquals('hi', $request->getResponse()->getBody(true)); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cache/DefaultCacheStorageTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cache/DefaultCacheStorageTest.php new file mode 100644 index 000000000..701a0155f --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cache/DefaultCacheStorageTest.php @@ -0,0 +1,193 @@ + 'application/json')); + $response = new Response(200, array( + 'Content-Type' => 'application/json', + 'Connection' => 'close', + 'X-Foo' => 'Bar', + 'Vary' => 'Accept' + ), 'test'); + $s->cache($request, $response); + $data = $this->readAttribute($a, 'data'); + + return array( + 'cache' => $a, + 'adapter' => $c, + 'storage' => $s, + 'request' => $request, + 'response' => $response, + 'serialized' => end($data) + ); + } + + public function testReturnsNullForCacheMiss() + { + $cache = $this->getCache(); + $this->assertNull($cache['storage']->fetch(new Request('GET', 'http://test.com'))); + } + + public function testCachesRequests() + { + $cache = $this->getCache(); + $foundRequest = $foundBody = $bodyKey = false; + foreach ($this->readAttribute($cache['cache'], 'data') as $key => $v) { + if (strpos($v, 'foo.com')) { + $foundRequest = true; + $data = unserialize($v); + $bodyKey = $data[0][3]; + $this->assertInternalType('integer', $data[0][4]); + $this->assertFalse(isset($data[0][0]['connection'])); + $this->assertEquals('foo.com', $data[0][0]['host']); + } elseif ($v == 'test') { + $foundBody = $key; + } + } + $this->assertContains($bodyKey, $foundBody); + $this->assertTrue($foundRequest); + } + + public function testFetchesResponse() + { + $cache = $this->getCache(); + $response = $cache['storage']->fetch($cache['request']); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertFalse($response->hasHeader('Connection')); + $this->assertEquals('Bar', (string) $response->getHeader('X-Foo')); + $this->assertEquals('test', (string) $response->getBody()); + $this->assertTrue(in_array($cache['serialized'], $this->readAttribute($cache['cache'], 'data'))); + } + + public function testDeletesRequestItemsAndBody() + { + $cache = $this->getCache(); + $cache['storage']->delete($cache['request']); + $this->assertFalse(in_array('test', $this->readAttribute($cache['cache'], 'data'))); + $this->assertFalse(in_array($cache['serialized'], $this->readAttribute($cache['cache'], 'data'))); + } + + public function testCachesMultipleRequestsWithVary() + { + $cache = $this->getCache(); + $cache['request']->setHeader('Accept', 'application/xml'); + $response = $cache['response']->setHeader('Content-Type', 'application/xml'); + $response->setBody('123'); + $cache['storage']->cache($cache['request'], $response); + $data = $this->readAttribute($cache['cache'], 'data'); + foreach ($data as $v) { + if (strpos($v, 'foo.com')) { + $u = unserialize($v); + $this->assertEquals(2, count($u)); + $this->assertEquals($u[0][0]['accept'], 'application/xml'); + $this->assertEquals($u[0][1]['content-type'], 'application/xml'); + $this->assertEquals($u[1][0]['accept'], 'application/json'); + $this->assertEquals($u[1][1]['content-type'], 'application/json'); + $this->assertNotSame($u[0][3], $u[1][3]); + break; + } + } + } + + public function testPurgeRemovesAllMethodCaches() + { + $cache = $this->getCache(); + foreach (array('HEAD', 'POST', 'PUT', 'DELETE') as $method) { + $request = RequestFactory::getInstance()->cloneRequestWithMethod($cache['request'], $method); + $cache['storage']->cache($request, $cache['response']); + } + $cache['storage']->purge('http://foo.com'); + $this->assertFalse(in_array('test', $this->readAttribute($cache['cache'], 'data'))); + $this->assertFalse(in_array($cache['serialized'], $this->readAttribute($cache['cache'], 'data'))); + $this->assertEquals( + array('DoctrineNamespaceCacheKey[]'), + array_keys($this->readAttribute($cache['cache'], 'data')) + ); + } + + public function testRemovesExpiredResponses() + { + $cache = $this->getCache(); + $request = new Request('GET', 'http://xyz.com'); + $response = new Response(200, array('Age' => 1000, 'Cache-Control' => 'max-age=-10000')); + $cache['storage']->cache($request, $response); + $this->assertNull($cache['storage']->fetch($request)); + $data = $this->readAttribute($cache['cache'], 'data'); + $this->assertFalse(in_array('xyz.com', $data)); + $this->assertTrue(in_array($cache['serialized'], $data)); + } + + public function testUsesVaryToDetermineResult() + { + $cache = $this->getCache(); + $this->assertInstanceOf('Guzzle\Http\Message\Response', $cache['storage']->fetch($cache['request'])); + $request = new Request('GET', 'http://foo.com', array('Accept' => 'application/xml')); + $this->assertNull($cache['storage']->fetch($request)); + } + + public function testEnsuresResponseIsStillPresent() + { + $cache = $this->getCache(); + $data = $this->readAttribute($cache['cache'], 'data'); + $key = array_search('test', $data); + $cache['cache']->delete(substr($key, 1, -4)); + $this->assertNull($cache['storage']->fetch($cache['request'])); + } + + public function staleProvider() + { + return array( + array( + new Request('GET', 'http://foo.com', array('Accept' => 'foo')), + new Response(200, array('Cache-Control' => 'stale-if-error=100', 'Vary' => 'Accept')) + ), + array( + new Request('GET', 'http://foo.com', array('Accept' => 'foo')), + new Response(200, array('Cache-Control' => 'stale-if-error', 'Vary' => 'Accept')) + ) + ); + } + + /** + * @dataProvider staleProvider + */ + public function testUsesStaleTimeDirectiveForTtd($request, $response) + { + $cache = $this->getCache(); + $cache['storage']->cache($request, $response); + $data = $this->readAttribute($cache['cache'], 'data'); + foreach ($data as $v) { + if (strpos($v, 'foo.com')) { + $u = unserialize($v); + $this->assertGreaterThan($u[1][4], $u[0][4]); + break; + } + } + } + + public function testCanFilterCacheKeys() + { + $cache = $this->getCache(); + $cache['request']->getQuery()->set('auth', 'foo'); + $this->assertNull($cache['storage']->fetch($cache['request'])); + $cache['request']->getParams()->set('cache.key_filter', 'auth'); + $this->assertNotNull($cache['storage']->fetch($cache['request'])); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cache/DefaultCanCacheStrategyTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cache/DefaultCanCacheStrategyTest.php new file mode 100644 index 000000000..de4d182a2 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cache/DefaultCanCacheStrategyTest.php @@ -0,0 +1,40 @@ +assertTrue($strategy->canCacheRequest($request)); + } + + public function testDoesNotCacheNoStore() + { + $strategy = new DefaultCanCacheStrategy(); + $request = new Request('GET', 'http://foo.com', array('cache-control' => 'no-store')); + $this->assertFalse($strategy->canCacheRequest($request)); + } + + public function testCanCacheResponse() + { + $response = $this->getMockBuilder('Guzzle\Http\Message\Response') + ->setMethods(array('canCache')) + ->setConstructorArgs(array(200)) + ->getMock(); + $response->expects($this->once()) + ->method('canCache') + ->will($this->returnValue(true)); + $strategy = new DefaultCanCacheStrategy(); + $this->assertTrue($strategy->canCacheResponse($response)); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cache/DefaultRevalidationTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cache/DefaultRevalidationTest.php new file mode 100644 index 000000000..19bcecace --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cache/DefaultRevalidationTest.php @@ -0,0 +1,246 @@ +getHttpDate('-100 hours') . "\r\nContent-Length: 4\r\n\r\nData", + "HTTP/1.1 304 NOT MODIFIED\r\nCache-Control: max-age=2000000\r\nContent-Length: 0\r\n\r\n", + ), + // Forces revalidation that overwrites what is in cache + array( + false, + "\r\n", + "HTTP/1.1 200 OK\r\nCache-Control: must-revalidate, no-cache\r\nDate: " . $this->getHttpDate('-10 hours') . "\r\nContent-Length: 4\r\n\r\nData", + "HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nDatas", + "HTTP/1.1 200 OK\r\nContent-Length: 5\r\nDate: " . $this->getHttpDate('now') . "\r\n\r\nDatas" + ), + // Throws an exception during revalidation + array( + false, + "\r\n", + "HTTP/1.1 200 OK\r\nCache-Control: no-cache\r\nDate: " . $this->getHttpDate('-3 hours') . "\r\n\r\nData", + "HTTP/1.1 500 INTERNAL SERVER ERROR\r\nContent-Length: 0\r\n\r\n" + ), + // ETag mismatch + array( + false, + "\r\n", + "HTTP/1.1 200 OK\r\nCache-Control: no-cache\r\nETag: \"123\"\r\nDate: " . $this->getHttpDate('-10 hours') . "\r\n\r\nData", + "HTTP/1.1 304 NOT MODIFIED\r\nETag: \"123456\"\r\n\r\n", + ), + ); + } + + /** + * @dataProvider cacheRevalidationDataProvider + */ + public function testRevalidatesResponsesAgainstOriginServer($can, $request, $response, $validate = null, $result = null) + { + // Send some responses to the test server for cache validation + $server = $this->getServer(); + $server->flush(); + + if ($validate) { + $server->enqueue($validate); + } + + $request = RequestFactory::getInstance()->fromMessage("GET / HTTP/1.1\r\nHost: 127.0.0.1:" . $server->getPort() . "\r\n" . $request); + $response = Response::fromMessage($response); + $request->setClient(new Client()); + + $plugin = new CachePlugin(new DoctrineCacheAdapter(new ArrayCache())); + $this->assertEquals( + $can, + $plugin->canResponseSatisfyRequest($request, $response), + '-> ' . $request . "\n" . $response + ); + + if ($result) { + $result = Response::fromMessage($result); + $result->removeHeader('Date'); + $request->getResponse()->removeHeader('Date'); + $request->getResponse()->removeHeader('Connection'); + // Get rid of dates + $this->assertEquals((string) $result, (string) $request->getResponse()); + } + + if ($validate) { + $this->assertEquals(1, count($server->getReceivedRequests())); + } + } + + public function testHandles404RevalidationResponses() + { + $request = new Request('GET', 'http://foo.com'); + $request->setClient(new Client()); + $badResponse = new Response(404, array(), 'Oh no!'); + $badRequest = clone $request; + $badRequest->setResponse($badResponse, true); + $response = new Response(200, array(), 'foo'); + + // Seed the cache + $s = new DefaultCacheStorage(new DoctrineCacheAdapter(new ArrayCache())); + $s->cache($request, $response); + $this->assertNotNull($s->fetch($request)); + + $rev = $this->getMockBuilder('Guzzle\Plugin\Cache\DefaultRevalidation') + ->setConstructorArgs(array($s)) + ->setMethods(array('createRevalidationRequest')) + ->getMock(); + + $rev->expects($this->once()) + ->method('createRevalidationRequest') + ->will($this->returnValue($badRequest)); + + try { + $rev->revalidate($request, $response); + $this->fail('Should have thrown an exception'); + } catch (BadResponseException $e) { + $this->assertSame($badResponse, $e->getResponse()); + $this->assertNull($s->fetch($request)); + } + } + + public function testCanRevalidateWithPlugin() + { + $this->getServer()->flush(); + $this->getServer()->enqueue(array( + "HTTP/1.1 200 OK\r\n" . + "Date: Mon, 12 Nov 2012 03:06:37 GMT\r\n" . + "Cache-Control: private, s-maxage=0, max-age=0, must-revalidate\r\n" . + "Last-Modified: Mon, 12 Nov 2012 02:53:38 GMT\r\n" . + "Content-Length: 2\r\n\r\nhi", + "HTTP/1.0 304 Not Modified\r\n" . + "Date: Mon, 12 Nov 2012 03:06:38 GMT\r\n" . + "Content-Type: text/html; charset=UTF-8\r\n" . + "Last-Modified: Mon, 12 Nov 2012 02:53:38 GMT\r\n" . + "Age: 6302\r\n\r\n", + "HTTP/1.0 304 Not Modified\r\n" . + "Date: Mon, 12 Nov 2012 03:06:38 GMT\r\n" . + "Content-Type: text/html; charset=UTF-8\r\n" . + "Last-Modified: Mon, 12 Nov 2012 02:53:38 GMT\r\n" . + "Age: 6302\r\n\r\n", + )); + $client = new Client($this->getServer()->getUrl()); + $client->addSubscriber(new CachePlugin()); + $this->assertEquals(200, $client->get()->send()->getStatusCode()); + $this->assertEquals(200, $client->get()->send()->getStatusCode()); + $this->assertEquals(200, $client->get()->send()->getStatusCode()); + $this->assertEquals(3, count($this->getServer()->getReceivedRequests())); + } + + public function testCanHandleRevalidationFailures() + { + $client = new Client($this->getServer()->getUrl()); + $lm = gmdate('c', time() - 60); + $mock = new MockPlugin(array( + new Response(200, array( + 'Date' => $lm, + 'Cache-Control' => 'max-age=100, must-revalidate, stale-if-error=9999', + 'Last-Modified' => $lm, + 'Content-Length' => 2 + ), 'hi'), + new CurlException('Bleh') + )); + $client->addSubscriber(new CachePlugin()); + $client->addSubscriber($mock); + $client->get()->send(); + $response = $client->get()->send(); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('hi', $response->getBody(true)); + $this->assertEquals(2, count($mock->getReceivedRequests())); + $this->assertEquals(0, count($mock->getQueue())); + } + + public function testCanHandleStaleIfErrorWhenRevalidating() + { + $lm = gmdate('c', time() - 60); + $mock = new MockPlugin(array( + new Response(200, array( + 'Date' => $lm, + 'Cache-Control' => 'must-revalidate, max-age=0, stale-if-error=1200', + 'Last-Modified' => $lm, + 'Content-Length' => 2 + ), 'hi'), + new CurlException('Oh no!') + )); + $cache = new CachePlugin(); + $client = new Client('http://www.example.com'); + $client->addSubscriber($cache); + $client->addSubscriber($mock); + $this->assertEquals(200, $client->get()->send()->getStatusCode()); + $response = $client->get()->send(); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertCount(0, $mock); + $this->assertEquals('HIT from GuzzleCache', (string) $response->getHeader('X-Cache-Lookup')); + $this->assertEquals('HIT_ERROR from GuzzleCache', (string) $response->getHeader('X-Cache')); + } + + /** + * @group issue-437 + */ + public function testDoesNotTouchClosureListeners() + { + $this->getServer()->flush(); + $this->getServer()->enqueue(array( + "HTTP/1.1 200 OK\r\n" . + "Date: Mon, 12 Nov 2012 03:06:37 GMT\r\n" . + "Cache-Control: private, s-maxage=0, max-age=0, must-revalidate\r\n" . + "Last-Modified: Mon, 12 Nov 2012 02:53:38 GMT\r\n" . + "Content-Length: 2\r\n\r\nhi", + "HTTP/1.0 304 Not Modified\r\n" . + "Date: Mon, 12 Nov 2012 03:06:38 GMT\r\n" . + "Content-Type: text/html; charset=UTF-8\r\n" . + "Last-Modified: Mon, 12 Nov 2012 02:53:38 GMT\r\n" . + "Age: 6302\r\n\r\n", + "HTTP/1.0 304 Not Modified\r\n" . + "Date: Mon, 12 Nov 2012 03:06:38 GMT\r\n" . + "Content-Type: text/html; charset=UTF-8\r\n" . + "Last-Modified: Mon, 12 Nov 2012 02:53:38 GMT\r\n" . + "Age: 6302\r\n\r\n", + )); + $client = new Client($this->getServer()->getUrl()); + $client->addSubscriber(new CachePlugin()); + $client->getEventDispatcher()->addListener('command.after_send', function(){}); + $this->assertEquals(200, $client->get()->send()->getStatusCode()); + $this->assertEquals(200, $client->get()->send()->getStatusCode()); + $this->assertEquals(200, $client->get()->send()->getStatusCode()); + } + +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cache/DenyRevalidationTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cache/DenyRevalidationTest.php new file mode 100644 index 000000000..9af80f255 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cache/DenyRevalidationTest.php @@ -0,0 +1,19 @@ +assertFalse($deny->revalidate(new Request('GET', 'http://foo.com'), new Response(200))); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cache/SkipRevalidationTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cache/SkipRevalidationTest.php new file mode 100644 index 000000000..4bcc04bfa --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cache/SkipRevalidationTest.php @@ -0,0 +1,19 @@ +assertTrue($skip->revalidate(new Request('GET', 'http://foo.com'), new Response(200))); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cookie/CookieJar/ArrayCookieJarTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cookie/CookieJar/ArrayCookieJarTest.php new file mode 100644 index 000000000..32897d084 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cookie/CookieJar/ArrayCookieJarTest.php @@ -0,0 +1,353 @@ +jar = new ArrayCookieJar(); + } + + protected function getTestCookies() + { + return array( + new Cookie(array('name' => 'foo', 'value' => 'bar', 'domain' => 'foo.com', 'path' => '/', 'discard' => true)), + new Cookie(array('name' => 'test', 'value' => '123', 'domain' => 'baz.com', 'path' => '/foo', 'expires' => 2)), + new Cookie(array('name' => 'you', 'value' => '123', 'domain' => 'bar.com', 'path' => '/boo', 'expires' => time() + 1000)) + ); + } + + /** + * Provides test data for cookie cookieJar retrieval + */ + public function getCookiesDataProvider() + { + return array( + array(array('foo', 'baz', 'test', 'muppet', 'googoo'), '', '', '', false), + array(array('foo', 'baz', 'muppet', 'googoo'), '', '', '', true), + array(array('googoo'), 'www.example.com', '', '', false), + array(array('muppet', 'googoo'), 'test.y.example.com', '', '', false), + array(array('foo', 'baz'), 'example.com', '', '', false), + array(array('muppet'), 'x.y.example.com', '/acme/', '', false), + array(array('muppet'), 'x.y.example.com', '/acme/test/', '', false), + array(array('googoo'), 'x.y.example.com', '/test/acme/test/', '', false), + array(array('foo', 'baz'), 'example.com', '', '', false), + array(array('baz'), 'example.com', '', 'baz', false), + ); + } + + public function testStoresAndRetrievesCookies() + { + $cookies = $this->getTestCookies(); + foreach ($cookies as $cookie) { + $this->assertTrue($this->jar->add($cookie)); + } + + $this->assertEquals(3, count($this->jar)); + $this->assertEquals(3, count($this->jar->getIterator())); + $this->assertEquals($cookies, $this->jar->all(null, null, null, false, false)); + } + + public function testRemovesExpiredCookies() + { + $cookies = $this->getTestCookies(); + foreach ($this->getTestCookies() as $cookie) { + $this->jar->add($cookie); + } + $this->jar->removeExpired(); + $this->assertEquals(array($cookies[0], $cookies[2]), $this->jar->all()); + } + + public function testRemovesTemporaryCookies() + { + $cookies = $this->getTestCookies(); + foreach ($this->getTestCookies() as $cookie) { + $this->jar->add($cookie); + } + $this->jar->removeTemporary(); + $this->assertEquals(array($cookies[2]), $this->jar->all()); + } + + public function testIsSerializable() + { + $this->assertEquals('[]', $this->jar->serialize()); + $this->jar->unserialize('[]'); + $this->assertEquals(array(), $this->jar->all()); + + $cookies = $this->getTestCookies(); + foreach ($this->getTestCookies() as $cookie) { + $this->jar->add($cookie); + } + + // Remove discard and expired cookies + $serialized = $this->jar->serialize(); + $data = json_decode($serialized, true); + $this->assertEquals(1, count($data)); + + $a = new ArrayCookieJar(); + $a->unserialize($serialized); + $this->assertEquals(1, count($a)); + } + + public function testRemovesSelectively() + { + $cookies = $this->getTestCookies(); + foreach ($this->getTestCookies() as $cookie) { + $this->jar->add($cookie); + } + + // Remove foo.com cookies + $this->jar->remove('foo.com'); + $this->assertEquals(2, count($this->jar)); + // Try again, removing no further cookies + $this->jar->remove('foo.com'); + $this->assertEquals(2, count($this->jar)); + + // Remove bar.com cookies with path of /boo + $this->jar->remove('bar.com', '/boo'); + $this->assertEquals(1, count($this->jar)); + + // Remove cookie by name + $this->jar->remove(null, null, 'test'); + $this->assertEquals(0, count($this->jar)); + } + + public function testDoesNotAddIncompleteCookies() + { + $this->assertEquals(false, $this->jar->add(new Cookie())); + $this->assertFalse($this->jar->add(new Cookie(array( + 'name' => 'foo' + )))); + $this->assertFalse($this->jar->add(new Cookie(array( + 'name' => false + )))); + $this->assertFalse($this->jar->add(new Cookie(array( + 'name' => true + )))); + $this->assertFalse($this->jar->add(new Cookie(array( + 'name' => 'foo', + 'domain' => 'foo.com' + )))); + } + + public function testDoesAddValidCookies() + { + $this->assertTrue($this->jar->add(new Cookie(array( + 'name' => 'foo', + 'domain' => 'foo.com', + 'value' => 0 + )))); + $this->assertTrue($this->jar->add(new Cookie(array( + 'name' => 'foo', + 'domain' => 'foo.com', + 'value' => 0.0 + )))); + $this->assertTrue($this->jar->add(new Cookie(array( + 'name' => 'foo', + 'domain' => 'foo.com', + 'value' => '0' + )))); + } + + public function testOverwritesCookiesThatAreOlderOrDiscardable() + { + $t = time() + 1000; + $data = array( + 'name' => 'foo', + 'value' => 'bar', + 'domain' => '.example.com', + 'path' => '/', + 'max_age' => '86400', + 'port' => array(80, 8080), + 'version' => '1', + 'secure' => true, + 'discard' => true, + 'expires' => $t + ); + + // Make sure that the discard cookie is overridden with the non-discard + $this->assertTrue($this->jar->add(new Cookie($data))); + + unset($data['discard']); + $this->assertTrue($this->jar->add(new Cookie($data))); + $this->assertEquals(1, count($this->jar)); + + $c = $this->jar->all(); + $this->assertEquals(false, $c[0]->getDiscard()); + + // Make sure it doesn't duplicate the cookie + $this->jar->add(new Cookie($data)); + $this->assertEquals(1, count($this->jar)); + + // Make sure the more future-ful expiration date supersede the other + $data['expires'] = time() + 2000; + $this->assertTrue($this->jar->add(new Cookie($data))); + $this->assertEquals(1, count($this->jar)); + $c = $this->jar->all(); + $this->assertNotEquals($t, $c[0]->getExpires()); + } + + public function testOverwritesCookiesThatHaveChanged() + { + $t = time() + 1000; + $data = array( + 'name' => 'foo', + 'value' => 'bar', + 'domain' => '.example.com', + 'path' => '/', + 'max_age' => '86400', + 'port' => array(80, 8080), + 'version' => '1', + 'secure' => true, + 'discard' => true, + 'expires' => $t + ); + + // Make sure that the discard cookie is overridden with the non-discard + $this->assertTrue($this->jar->add(new Cookie($data))); + + $data['value'] = 'boo'; + $this->assertTrue($this->jar->add(new Cookie($data))); + $this->assertEquals(1, count($this->jar)); + + // Changing the value plus a parameter also must overwrite the existing one + $data['value'] = 'zoo'; + $data['secure'] = false; + $this->assertTrue($this->jar->add(new Cookie($data))); + $this->assertEquals(1, count($this->jar)); + + $c = $this->jar->all(); + $this->assertEquals('zoo', $c[0]->getValue()); + } + + public function testAddsCookiesFromResponseWithNoRequest() + { + $response = new Response(200, array( + 'Set-Cookie' => array( + "fpc=d=.Hm.yh4.1XmJWjJfs4orLQzKzPImxklQoxXSHOZATHUSEFciRueW_7704iYUtsXNEXq0M92Px2glMdWypmJ7HIQl6XIUvrZimWjQ3vIdeuRbI.FNQMAfcxu_XN1zSx7l.AcPdKL6guHc2V7hIQFhnjRW0rxm2oHY1P4bGQxFNz7f.tHm12ZD3DbdMDiDy7TBXsuP4DM-&v=2; expires=Fri, 02-Mar-2019 02:17:40 GMT; path=/; domain=127.0.0.1", + "FPCK3=AgBNbvoQAGpGEABZLRAAbFsQAF1tEABkDhAAeO0=; expires=Sat, 02-Apr-2019 02:17:40 GMT; path=/; domain=127.0.0.1", + "CH=deleted; expires=Wed, 03-Mar-2010 02:17:39 GMT; path=/; domain=127.0.0.1", + "CH=AgBNbvoQAAEcEAApuhAAMJcQADQvEAAvGxAALe0QAD6uEAATwhAAC1AQAC8t; expires=Sat, 02-Apr-2019 02:17:40 GMT; path=/; domain=127.0.0.1" + ) + )); + + $this->jar->addCookiesFromResponse($response); + $this->assertEquals(3, count($this->jar)); + $this->assertEquals(1, count($this->jar->all(null, null, 'fpc'))); + $this->assertEquals(1, count($this->jar->all(null, null, 'FPCK3'))); + $this->assertEquals(1, count($this->jar->all(null, null, 'CH'))); + } + + public function testAddsCookiesFromResponseWithRequest() + { + $response = new Response(200, array( + 'Set-Cookie' => "fpc=d=.Hm.yh4.1XmJWjJfs4orLQzKzPImxklQoxXSHOZATHUSEFciRueW_7704iYUtsXNEXq0M92Px2glMdWypmJ7HIQl6XIUvrZimWjQ3vIdeuRbI.FNQMAfcxu_XN1zSx7l.AcPdKL6guHc2V7hIQFhnjRW0rxm2oHY1P4bGQxFNz7f.tHm12ZD3DbdMDiDy7TBXsuP4DM-&v=2; expires=Fri, 02-Mar-2019 02:17:40 GMT;" + )); + $request = new Request('GET', 'http://www.example.com'); + $this->jar->addCookiesFromResponse($response, $request); + $this->assertEquals(1, count($this->jar)); + } + + public function getMatchingCookiesDataProvider() + { + return array( + array('https://example.com', array(0)), + array('http://example.com', array()), + array('https://example.com:8912', array()), + array('https://foo.example.com', array(0)), + array('http://foo.example.com/test/acme/', array(4)) + ); + } + + /** + * @dataProvider getMatchingCookiesDataProvider + */ + public function testReturnsCookiesMatchingRequests($url, $cookies) + { + $bag = array( + new Cookie(array( + 'name' => 'foo', + 'value' => 'bar', + 'domain' => 'example.com', + 'path' => '/', + 'max_age' => '86400', + 'port' => array(443, 8080), + 'version' => '1', + 'secure' => true + )), + new Cookie(array( + 'name' => 'baz', + 'value' => 'foobar', + 'domain' => 'example.com', + 'path' => '/', + 'max_age' => '86400', + 'port' => array(80, 8080), + 'version' => '1', + 'secure' => true + )), + new Cookie(array( + 'name' => 'test', + 'value' => '123', + 'domain' => 'www.foobar.com', + 'path' => '/path/', + 'discard' => true + )), + new Cookie(array( + 'name' => 'muppet', + 'value' => 'cookie_monster', + 'domain' => '.y.example.com', + 'path' => '/acme/', + 'comment' => 'Comment goes here...', + 'expires' => time() + 86400 + )), + new Cookie(array( + 'name' => 'googoo', + 'value' => 'gaga', + 'domain' => '.example.com', + 'path' => '/test/acme/', + 'max_age' => 1500, + 'version' => 2 + )) + ); + + foreach ($bag as $cookie) { + $this->jar->add($cookie); + } + + $request = new Request('GET', $url); + $results = $this->jar->getMatchingCookies($request); + $this->assertEquals(count($cookies), count($results)); + foreach ($cookies as $i) { + $this->assertContains($bag[$i], $results); + } + } + + /** + * @expectedException \Guzzle\Plugin\Cookie\Exception\InvalidCookieException + * @expectedExceptionMessage The cookie name must not contain invalid characters: abc:@123 + */ + public function testThrowsExceptionWithStrictMode() + { + $a = new ArrayCookieJar(); + $a->setStrictMode(true); + $a->add(new Cookie(array( + 'name' => 'abc:@123', + 'value' => 'foo', + 'domain' => 'bar' + ))); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cookie/CookieJar/FileCookieJarTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cookie/CookieJar/FileCookieJarTest.php new file mode 100644 index 000000000..ac9471fd8 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cookie/CookieJar/FileCookieJarTest.php @@ -0,0 +1,63 @@ +file = tempnam('/tmp', 'file-cookies'); + } + + public function testLoadsFromFileFile() + { + $jar = new FileCookieJar($this->file); + $this->assertEquals(array(), $jar->all()); + unlink($this->file); + } + + public function testPersistsToFileFile() + { + $jar = new FileCookieJar($this->file); + $jar->add(new Cookie(array( + 'name' => 'foo', + 'value' => 'bar', + 'domain' => 'foo.com', + 'expires' => time() + 1000 + ))); + $jar->add(new Cookie(array( + 'name' => 'baz', + 'value' => 'bar', + 'domain' => 'foo.com', + 'expires' => time() + 1000 + ))); + $jar->add(new Cookie(array( + 'name' => 'boo', + 'value' => 'bar', + 'domain' => 'foo.com', + ))); + + $this->assertEquals(3, count($jar)); + unset($jar); + + // Make sure it wrote to the file + $contents = file_get_contents($this->file); + $this->assertNotEmpty($contents); + + // Load the cookieJar from the file + $jar = new FileCookieJar($this->file); + + // Weeds out temporary and session cookies + $this->assertEquals(2, count($jar)); + unset($jar); + unlink($this->file); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cookie/CookiePluginTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cookie/CookiePluginTest.php new file mode 100644 index 000000000..f8c175cce --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cookie/CookiePluginTest.php @@ -0,0 +1,134 @@ +getMockBuilder('Guzzle\Plugin\Cookie\CookieJar\ArrayCookieJar') + ->setMethods(array('addCookiesFromResponse')) + ->getMock(); + + $mock->expects($this->exactly(1)) + ->method('addCookiesFromResponse') + ->with($response); + + $plugin = new CookiePlugin($mock); + $plugin->onRequestSent(new Event(array( + 'response' => $response + ))); + } + + public function testAddsCookiesToRequests() + { + $cookie = new Cookie(array( + 'name' => 'foo', + 'value' => 'bar' + )); + + $mock = $this->getMockBuilder('Guzzle\Plugin\Cookie\CookieJar\ArrayCookieJar') + ->setMethods(array('getMatchingCookies')) + ->getMock(); + + $mock->expects($this->once()) + ->method('getMatchingCookies') + ->will($this->returnValue(array($cookie))); + + $plugin = new CookiePlugin($mock); + + $client = new Client(); + $client->getEventDispatcher()->addSubscriber($plugin); + + $request = $client->get('http://www.example.com'); + $plugin->onRequestBeforeSend(new Event(array( + 'request' => $request + ))); + + $this->assertEquals('bar', $request->getCookie('foo')); + } + + public function testCookiesAreExtractedFromRedirectResponses() + { + $plugin = new CookiePlugin(new ArrayCookieJar()); + $this->getServer()->flush(); + $this->getServer()->enqueue(array( + "HTTP/1.1 302 Moved Temporarily\r\n" . + "Set-Cookie: test=583551; expires=Wednesday, 23-Mar-2050 19:49:45 GMT; path=/\r\n" . + "Location: /redirect\r\n\r\n", + "HTTP/1.1 200 OK\r\n" . + "Content-Length: 0\r\n\r\n", + "HTTP/1.1 200 OK\r\n" . + "Content-Length: 0\r\n\r\n" + )); + + $client = new Client($this->getServer()->getUrl()); + $client->getEventDispatcher()->addSubscriber($plugin); + + $client->get()->send(); + $request = $client->get(); + $request->send(); + $this->assertEquals('test=583551', $request->getHeader('Cookie')); + + $requests = $this->getServer()->getReceivedRequests(true); + // Confirm subsequent requests have the cookie. + $this->assertEquals('test=583551', $requests[2]->getHeader('Cookie')); + // Confirm the redirected request has the cookie. + $this->assertEquals('test=583551', $requests[1]->getHeader('Cookie')); + } + + public function testCookiesAreNotAddedWhenParamIsSet() + { + $jar = new ArrayCookieJar(); + $plugin = new CookiePlugin($jar); + + $jar->add(new Cookie(array( + 'domain' => 'example.com', + 'path' => '/', + 'name' => 'test', + 'value' => 'hi', + 'expires' => time() + 3600 + ))); + + $client = new Client('http://example.com'); + $client->getEventDispatcher()->addSubscriber($plugin); + + // Ensure that it is normally added + $request = $client->get(); + $request->setResponse(new Response(200), true); + $request->send(); + $this->assertEquals('hi', $request->getCookie('test')); + + // Now ensure that it is not added + $request = $client->get(); + $request->getParams()->set('cookies.disable', true); + $request->setResponse(new Response(200), true); + $request->send(); + $this->assertNull($request->getCookie('test')); + } + + public function testProvidesCookieJar() + { + $jar = new ArrayCookieJar(); + $plugin = new CookiePlugin($jar); + $this->assertSame($jar, $plugin->getCookieJar()); + } + + public function testEscapesCookieDomains() + { + $cookie = new Cookie(array('domain' => '/foo/^$[A-Z]+/')); + $this->assertFalse($cookie->matchesDomain('foo')); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cookie/CookieTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cookie/CookieTest.php new file mode 100644 index 000000000..3d8b0e9bb --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Cookie/CookieTest.php @@ -0,0 +1,208 @@ +assertEquals('/', $cookie->getPath()); + $this->assertEquals(array(), $cookie->getPorts()); + } + + public function testConvertsDateTimeMaxAgeToUnixTimestamp() + { + $cookie = new Cookie(array( + 'expires' => 'November 20, 1984' + )); + $this->assertTrue(is_numeric($cookie->getExpires())); + } + + public function testAddsExpiresBasedOnMaxAge() + { + $t = time(); + $cookie = new Cookie(array( + 'max_age' => 100 + )); + $this->assertEquals($t + 100, $cookie->getExpires()); + } + + public function testHoldsValues() + { + $t = time(); + $data = array( + 'name' => 'foo', + 'value' => 'baz', + 'path' => '/bar', + 'domain' => 'baz.com', + 'expires' => $t, + 'max_age' => 100, + 'comment' => 'Hi', + 'comment_url' => 'foo.com', + 'port' => array(1, 2), + 'version' => 2, + 'secure' => true, + 'discard' => true, + 'http_only' => true, + 'data' => array( + 'foo' => 'baz', + 'bar' => 'bam' + ) + ); + + $cookie = new Cookie($data); + $this->assertEquals($data, $cookie->toArray()); + + $this->assertEquals('foo', $cookie->getName()); + $this->assertEquals('baz', $cookie->getValue()); + $this->assertEquals('baz.com', $cookie->getDomain()); + $this->assertEquals('/bar', $cookie->getPath()); + $this->assertEquals($t, $cookie->getExpires()); + $this->assertEquals(100, $cookie->getMaxAge()); + $this->assertEquals('Hi', $cookie->getComment()); + $this->assertEquals('foo.com', $cookie->getCommentUrl()); + $this->assertEquals(array(1, 2), $cookie->getPorts()); + $this->assertEquals(2, $cookie->getVersion()); + $this->assertTrue($cookie->getSecure()); + $this->assertTrue($cookie->getDiscard()); + $this->assertTrue($cookie->getHttpOnly()); + $this->assertEquals('baz', $cookie->getAttribute('foo')); + $this->assertEquals('bam', $cookie->getAttribute('bar')); + $this->assertEquals(array( + 'foo' => 'baz', + 'bar' => 'bam' + ), $cookie->getAttributes()); + + $cookie->setName('a') + ->setValue('b') + ->setPath('c') + ->setDomain('bar.com') + ->setExpires(10) + ->setMaxAge(200) + ->setComment('e') + ->setCommentUrl('f') + ->setPorts(array(80)) + ->setVersion(3) + ->setSecure(false) + ->setHttpOnly(false) + ->setDiscard(false) + ->setAttribute('snoop', 'dog'); + + $this->assertEquals('a', $cookie->getName()); + $this->assertEquals('b', $cookie->getValue()); + $this->assertEquals('c', $cookie->getPath()); + $this->assertEquals('bar.com', $cookie->getDomain()); + $this->assertEquals(10, $cookie->getExpires()); + $this->assertEquals(200, $cookie->getMaxAge()); + $this->assertEquals('e', $cookie->getComment()); + $this->assertEquals('f', $cookie->getCommentUrl()); + $this->assertEquals(array(80), $cookie->getPorts()); + $this->assertEquals(3, $cookie->getVersion()); + $this->assertFalse($cookie->getSecure()); + $this->assertFalse($cookie->getDiscard()); + $this->assertFalse($cookie->getHttpOnly()); + $this->assertEquals('dog', $cookie->getAttribute('snoop')); + } + + public function testDeterminesIfExpired() + { + $c = new Cookie(); + $c->setExpires(10); + $this->assertTrue($c->isExpired()); + $c->setExpires(time() + 10000); + $this->assertFalse($c->isExpired()); + } + + public function testMatchesPorts() + { + $cookie = new Cookie(); + // Always matches when nothing is set + $this->assertTrue($cookie->matchesPort(2)); + + $cookie->setPorts(array(1, 2)); + $this->assertTrue($cookie->matchesPort(2)); + $this->assertFalse($cookie->matchesPort(100)); + } + + public function testMatchesDomain() + { + $cookie = new Cookie(); + $this->assertTrue($cookie->matchesDomain('baz.com')); + + $cookie->setDomain('baz.com'); + $this->assertTrue($cookie->matchesDomain('baz.com')); + $this->assertFalse($cookie->matchesDomain('bar.com')); + + $cookie->setDomain('.baz.com'); + $this->assertTrue($cookie->matchesDomain('.baz.com')); + $this->assertTrue($cookie->matchesDomain('foo.baz.com')); + $this->assertFalse($cookie->matchesDomain('baz.bar.com')); + $this->assertTrue($cookie->matchesDomain('baz.com')); + + $cookie->setDomain('.127.0.0.1'); + $this->assertTrue($cookie->matchesDomain('127.0.0.1')); + + $cookie->setDomain('127.0.0.1'); + $this->assertTrue($cookie->matchesDomain('127.0.0.1')); + + $cookie->setDomain('.com.'); + $this->assertFalse($cookie->matchesDomain('baz.com')); + + $cookie->setDomain('.local'); + $this->assertTrue($cookie->matchesDomain('example.local')); + } + + public function testMatchesPath() + { + $cookie = new Cookie(); + $this->assertTrue($cookie->matchesPath('/foo')); + + $cookie->setPath('/foo'); + $this->assertTrue($cookie->matchesPath('/foo')); + $this->assertTrue($cookie->matchesPath('/foo/bar')); + $this->assertFalse($cookie->matchesPath('/bar')); + } + + public function cookieValidateProvider() + { + return array( + array('foo', 'baz', 'bar', true), + array('0', '0', '0', true), + array('', 'baz', 'bar', 'The cookie name must not be empty'), + array('foo', '', 'bar', 'The cookie value must not be empty'), + array('foo', 'baz', '', 'The cookie domain must not be empty'), + array('foo\\', 'baz', '0', 'The cookie name must not contain invalid characters: foo\\'), + ); + } + + /** + * @dataProvider cookieValidateProvider + */ + public function testValidatesCookies($name, $value, $domain, $result) + { + $cookie = new Cookie(array( + 'name' => $name, + 'value' => $value, + 'domain' => $domain + )); + $this->assertSame($result, $cookie->validate()); + } + + public function testCreatesInvalidCharacterString() + { + $m = new \ReflectionMethod('Guzzle\Plugin\Cookie\Cookie', 'getInvalidCharacters'); + $m->setAccessible(true); + $p = new \ReflectionProperty('Guzzle\Plugin\Cookie\Cookie', 'invalidCharString'); + $p->setAccessible(true); + $p->setValue(''); + // Expects a string containing 51 invalid characters + $this->assertEquals(51, strlen($m->invoke($m))); + $this->assertContains('@', $m->invoke($m)); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Plugin/CurlAuth/CurlAuthPluginTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/CurlAuth/CurlAuthPluginTest.php new file mode 100644 index 000000000..2a4b49eb6 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/CurlAuth/CurlAuthPluginTest.php @@ -0,0 +1,39 @@ +getEventDispatcher()->addSubscriber($plugin); + $request = $client->get('/'); + $this->assertEquals('michael', $request->getUsername()); + $this->assertEquals('test', $request->getPassword()); + Version::$emitWarnings = true; + } + + public function testAddsDigestAuthentication() + { + Version::$emitWarnings = false; + $plugin = new CurlAuthPlugin('julian', 'test', CURLAUTH_DIGEST); + $client = new Client('http://www.test.com/'); + $client->getEventDispatcher()->addSubscriber($plugin); + $request = $client->get('/'); + $this->assertEquals('julian', $request->getUsername()); + $this->assertEquals('test', $request->getPassword()); + $this->assertEquals('julian:test', $request->getCurlOptions()->get(CURLOPT_USERPWD)); + $this->assertEquals(CURLAUTH_DIGEST, $request->getCurlOptions()->get(CURLOPT_HTTPAUTH)); + Version::$emitWarnings = true; + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Plugin/ErrorResponse/ErrorResponsePluginTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/ErrorResponse/ErrorResponsePluginTest.php new file mode 100644 index 000000000..6f94186b5 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/ErrorResponse/ErrorResponsePluginTest.php @@ -0,0 +1,137 @@ +flush(); + } + + public function setUp() + { + $mockError = 'Guzzle\Tests\Mock\ErrorResponseMock'; + $description = ServiceDescription::factory(array( + 'operations' => array( + 'works' => array( + 'httpMethod' => 'GET', + 'errorResponses' => array( + array('code' => 500, 'class' => $mockError), + array('code' => 503, 'reason' => 'foo', 'class' => $mockError), + array('code' => 200, 'reason' => 'Error!', 'class' => $mockError) + ) + ), + 'bad_class' => array( + 'httpMethod' => 'GET', + 'errorResponses' => array( + array('code' => 500, 'class' => 'Does\\Not\\Exist') + ) + ), + 'does_not_implement' => array( + 'httpMethod' => 'GET', + 'errorResponses' => array( + array('code' => 500, 'class' => __CLASS__) + ) + ), + 'no_errors' => array('httpMethod' => 'GET'), + 'no_class' => array( + 'httpMethod' => 'GET', + 'errorResponses' => array( + array('code' => 500) + ) + ), + ) + )); + $this->client = new Client($this->getServer()->getUrl()); + $this->client->setDescription($description); + } + + /** + * @expectedException \Guzzle\Http\Exception\ServerErrorResponseException + */ + public function testSkipsWhenErrorResponsesIsNotSet() + { + $this->getServer()->enqueue("HTTP/1.1 500 Foo\r\nContent-Length: 0\r\n\r\n"); + $this->client->addSubscriber(new ErrorResponsePlugin()); + $this->client->getCommand('no_errors')->execute(); + } + + public function testSkipsWhenErrorResponsesIsNotSetAndAllowsSuccess() + { + $this->getServer()->enqueue("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); + $this->client->addSubscriber(new ErrorResponsePlugin()); + $this->client->getCommand('no_errors')->execute(); + } + + /** + * @expectedException \Guzzle\Plugin\ErrorResponse\Exception\ErrorResponseException + * @expectedExceptionMessage Does\Not\Exist does not exist + */ + public function testEnsuresErrorResponseExists() + { + $this->getServer()->enqueue("HTTP/1.1 500 Foo\r\nContent-Length: 0\r\n\r\n"); + $this->client->addSubscriber(new ErrorResponsePlugin()); + $this->client->getCommand('bad_class')->execute(); + } + + /** + * @expectedException \Guzzle\Plugin\ErrorResponse\Exception\ErrorResponseException + * @expectedExceptionMessage must implement Guzzle\Plugin\ErrorResponse\ErrorResponseExceptionInterface + */ + public function testEnsuresErrorResponseImplementsInterface() + { + $this->getServer()->enqueue("HTTP/1.1 500 Foo\r\nContent-Length: 0\r\n\r\n"); + $this->client->addSubscriber(new ErrorResponsePlugin()); + $this->client->getCommand('does_not_implement')->execute(); + } + + public function testThrowsSpecificErrorResponseOnMatch() + { + try { + $this->getServer()->enqueue("HTTP/1.1 500 Foo\r\nContent-Length: 0\r\n\r\n"); + $this->client->addSubscriber(new ErrorResponsePlugin()); + $command = $this->client->getCommand('works'); + $command->execute(); + $this->fail('Exception not thrown'); + } catch (ErrorResponseMock $e) { + $this->assertSame($command, $e->command); + $this->assertEquals(500, $e->response->getStatusCode()); + } + } + + /** + * @expectedException \Guzzle\Tests\Mock\ErrorResponseMock + */ + public function testThrowsWhenCodeAndPhraseMatch() + { + $this->getServer()->enqueue("HTTP/1.1 200 Error!\r\nContent-Length: 0\r\n\r\n"); + $this->client->addSubscriber(new ErrorResponsePlugin()); + $this->client->getCommand('works')->execute(); + } + + public function testSkipsWhenReasonDoesNotMatch() + { + $this->getServer()->enqueue("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); + $this->client->addSubscriber(new ErrorResponsePlugin()); + $this->client->getCommand('works')->execute(); + } + + public function testSkipsWhenNoClassIsSet() + { + $this->getServer()->enqueue("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); + $this->client->addSubscriber(new ErrorResponsePlugin()); + $this->client->getCommand('no_class')->execute(); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Plugin/History/HistoryPluginTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/History/HistoryPluginTest.php new file mode 100644 index 000000000..84dc90df9 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/History/HistoryPluginTest.php @@ -0,0 +1,140 @@ +get(); + $requests[$i]->setResponse(new Response(200), true); + $requests[$i]->send(); + $h->add($requests[$i]); + } + + return $requests; + } + + public function testDescribesSubscribedEvents() + { + $this->assertInternalType('array', HistoryPlugin::getSubscribedEvents()); + } + + public function testMaintainsLimitValue() + { + $h = new HistoryPlugin(); + $this->assertSame($h, $h->setLimit(10)); + $this->assertEquals(10, $h->getLimit()); + } + + public function testAddsRequests() + { + $h = new HistoryPlugin(); + $requests = $this->addRequests($h, 1); + $this->assertEquals(1, count($h)); + $i = $h->getIterator(); + $this->assertEquals(1, count($i)); + $this->assertEquals($requests[0], $i[0]); + } + + /** + * @depends testAddsRequests + */ + public function testMaintainsLimit() + { + $h = new HistoryPlugin(); + $h->setLimit(2); + $requests = $this->addRequests($h, 3); + $this->assertEquals(2, count($h)); + $i = 0; + foreach ($h as $request) { + if ($i > 0) { + $this->assertSame($requests[$i], $request); + } + } + } + + public function testReturnsLastRequest() + { + $h = new HistoryPlugin(); + $requests = $this->addRequests($h, 5); + $this->assertSame(end($requests), $h->getLastRequest()); + } + + public function testReturnsLastResponse() + { + $h = new HistoryPlugin(); + $requests = $this->addRequests($h, 5); + $this->assertSame(end($requests)->getResponse(), $h->getLastResponse()); + } + + public function testClearsHistory() + { + $h = new HistoryPlugin(); + $requests = $this->addRequests($h, 5); + $this->assertEquals(5, count($h)); + $h->clear(); + $this->assertEquals(0, count($h)); + } + + /** + * @depends testAddsRequests + */ + public function testUpdatesAddRequests() + { + $h = new HistoryPlugin(); + $client = new Client('http://localhost/'); + $client->getEventDispatcher()->addSubscriber($h); + + $request = $client->get(); + $request->setResponse(new Response(200), true); + $request->send(); + + $this->assertSame($request, $h->getLastRequest()); + } + + public function testCanCastToString() + { + $client = new Client('http://localhost/'); + $h = new HistoryPlugin(); + $client->getEventDispatcher()->addSubscriber($h); + + $mock = new MockPlugin(array( + new Response(301, array('Location' => '/redirect1', 'Content-Length' => 0)), + new Response(307, array('Location' => '/redirect2', 'Content-Length' => 0)), + new Response(200, array('Content-Length' => '2'), 'HI') + )); + + $client->getEventDispatcher()->addSubscriber($mock); + $request = $client->get(); + $request->send(); + $this->assertEquals(3, count($h)); + $this->assertEquals(3, count($mock->getReceivedRequests())); + + $h = str_replace("\r", '', $h); + $this->assertContains("> GET / HTTP/1.1\nHost: localhost\nUser-Agent:", $h); + $this->assertContains("< HTTP/1.1 301 Moved Permanently\nLocation: /redirect1", $h); + $this->assertContains("< HTTP/1.1 307 Temporary Redirect\nLocation: /redirect2", $h); + $this->assertContains("< HTTP/1.1 200 OK\nContent-Length: 2\n\nHI", $h); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Log/LogPluginTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Log/LogPluginTest.php new file mode 100644 index 000000000..ad663a53c --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Log/LogPluginTest.php @@ -0,0 +1,95 @@ +adapter = new ClosureLogAdapter(function ($message) { + echo $message; + }); + } + + public function testIgnoresCurlEventsWhenNotWiringBodies() + { + $p = new LogPlugin($this->adapter); + $this->assertNotEmpty($p->getSubscribedEvents()); + $event = new Event(array('request' => new Request('GET', 'http://foo.com'))); + $p->onCurlRead($event); + $p->onCurlWrite($event); + $p->onRequestBeforeSend($event); + } + + public function testLogsWhenComplete() + { + $output = ''; + $p = new LogPlugin(new ClosureLogAdapter(function ($message) use (&$output) { + $output = $message; + }), '{method} {resource} | {code} {res_body}'); + + $p->onRequestSent(new Event(array( + 'request' => new Request('GET', 'http://foo.com'), + 'response' => new Response(200, array(), 'Foo') + ))); + + $this->assertEquals('GET / | 200 Foo', $output); + } + + public function testWiresBodiesWhenNeeded() + { + $client = new Client($this->getServer()->getUrl()); + $plugin = new LogPlugin($this->adapter, '{req_body} | {res_body}', true); + $client->getEventDispatcher()->addSubscriber($plugin); + $request = $client->put(); + + // Send the response from the dummy server as the request body + $this->getServer()->enqueue("HTTP/1.1 200 OK\r\nContent-Length: 4\r\n\r\nsend"); + $stream = fopen($this->getServer()->getUrl(), 'r'); + $request->setBody(EntityBody::factory($stream, 4)); + + $tmpFile = tempnam(sys_get_temp_dir(), 'non_repeatable'); + $request->setResponseBody(EntityBody::factory(fopen($tmpFile, 'w'))); + + $this->getServer()->enqueue("HTTP/1.1 200 OK\r\nContent-Length: 8\r\n\r\nresponse"); + + ob_start(); + $request->send(); + $message = ob_get_clean(); + + unlink($tmpFile); + $this->assertContains("send", $message); + $this->assertContains("response", $message); + } + + public function testHasHelpfulStaticFactoryMethod() + { + $s = fopen('php://temp', 'r+'); + $client = new Client(); + $client->addSubscriber(LogPlugin::getDebugPlugin(true, $s)); + $request = $client->put('http://foo.com', array('Content-Type' => 'Foo'), 'Bar'); + $request->setresponse(new Response(200), true); + $request->send(); + rewind($s); + $contents = stream_get_contents($s); + $this->assertContains('# Request:', $contents); + $this->assertContainsIns('PUT / HTTP/1.1', $contents); + $this->assertContains('# Response:', $contents); + $this->assertContainsIns('HTTP/1.1 200 OK', $contents); + $this->assertContains('# Errors:', $contents); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Md5/CommandContentMd5PluginTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Md5/CommandContentMd5PluginTest.php new file mode 100644 index 000000000..4bd411113 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Md5/CommandContentMd5PluginTest.php @@ -0,0 +1,97 @@ + array( + 'test' => array( + 'httpMethod' => 'PUT', + 'parameters' => array( + 'ContentMD5' => array(), + 'Body' => array( + 'location' => 'body' + ) + ) + ) + ) + )); + + $client = new Client(); + $client->setDescription($description); + + return $client; + } + + public function testHasEvents() + { + $this->assertNotEmpty(CommandContentMd5Plugin::getSubscribedEvents()); + } + + public function testValidatesMd5WhenParamExists() + { + $client = $this->getClient(); + $command = $client->getCommand('test', array( + 'Body' => 'Foo', + 'ContentMD5' => true + )); + $event = new Event(array('command' => $command)); + $request = $command->prepare(); + $plugin = new CommandContentMd5Plugin(); + $plugin->onCommandBeforeSend($event); + $this->assertEquals('E1bGfXrRY42Ba/uCLdLCXQ==', (string) $request->getHeader('Content-MD5')); + } + + public function testDoesNothingWhenNoPayloadExists() + { + $client = $this->getClient(); + $client->getDescription()->getOperation('test')->setHttpMethod('GET'); + $command = $client->getCommand('test'); + $event = new Event(array('command' => $command)); + $request = $command->prepare(); + $plugin = new CommandContentMd5Plugin(); + $plugin->onCommandBeforeSend($event); + $this->assertNull($request->getHeader('Content-MD5')); + } + + public function testAddsValidationToResponsesOfContentMd5() + { + $client = $this->getClient(); + $client->getDescription()->getOperation('test')->setHttpMethod('GET'); + $command = $client->getCommand('test', array( + 'ValidateMD5' => true + )); + $event = new Event(array('command' => $command)); + $request = $command->prepare(); + $plugin = new CommandContentMd5Plugin(); + $plugin->onCommandBeforeSend($event); + $listeners = $request->getEventDispatcher()->getListeners('request.complete'); + $this->assertNotEmpty($listeners); + } + + public function testIgnoresValidationWhenDisabled() + { + $client = $this->getClient(); + $client->getDescription()->getOperation('test')->setHttpMethod('GET'); + $command = $client->getCommand('test', array( + 'ValidateMD5' => false + )); + $event = new Event(array('command' => $command)); + $request = $command->prepare(); + $plugin = new CommandContentMd5Plugin(); + $plugin->onCommandBeforeSend($event); + $listeners = $request->getEventDispatcher()->getListeners('request.complete'); + $this->assertEmpty($listeners); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Md5/Md5ValidatorPluginTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Md5/Md5ValidatorPluginTest.php new file mode 100644 index 000000000..482e92b28 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Md5/Md5ValidatorPluginTest.php @@ -0,0 +1,120 @@ +create('GET', 'http://www.test.com/'); + $request->getEventDispatcher()->addSubscriber($plugin); + + $body = 'abc'; + $hash = md5($body); + $response = new Response(200, array( + 'Content-MD5' => $hash, + 'Content-Length' => 3 + ), 'abc'); + + $request->dispatch('request.complete', array( + 'response' => $response + )); + + // Try again with no Content-MD5 + $response->removeHeader('Content-MD5'); + $request->dispatch('request.complete', array( + 'response' => $response + )); + } + + /** + * @expectedException UnexpectedValueException + */ + public function testThrowsExceptionOnInvalidMd5() + { + $plugin = new Md5ValidatorPlugin(); + $request = RequestFactory::getInstance()->create('GET', 'http://www.test.com/'); + $request->getEventDispatcher()->addSubscriber($plugin); + + $request->dispatch('request.complete', array( + 'response' => new Response(200, array( + 'Content-MD5' => 'foobar', + 'Content-Length' => 3 + ), 'abc') + )); + } + + public function testSkipsWhenContentLengthIsTooLarge() + { + $plugin = new Md5ValidatorPlugin(false, 1); + $request = RequestFactory::getInstance()->create('GET', 'http://www.test.com/'); + $request->getEventDispatcher()->addSubscriber($plugin); + + $request->dispatch('request.complete', array( + 'response' => new Response(200, array( + 'Content-MD5' => 'foobar', + 'Content-Length' => 3 + ), 'abc') + )); + } + + public function testProperlyValidatesWhenUsingContentEncoding() + { + $plugin = new Md5ValidatorPlugin(true); + $request = RequestFactory::getInstance()->create('GET', 'http://www.test.com/'); + $request->getEventDispatcher()->addSubscriber($plugin); + + // Content-MD5 is the MD5 hash of the canonical content after all + // content-encoding has been applied. Because cURL will automatically + // decompress entity bodies, we need to re-compress it to calculate. + $body = EntityBody::factory('abc'); + $body->compress(); + $hash = $body->getContentMd5(); + $body->uncompress(); + + $response = new Response(200, array( + 'Content-MD5' => $hash, + 'Content-Encoding' => 'gzip' + ), 'abc'); + $request->dispatch('request.complete', array( + 'response' => $response + )); + $this->assertEquals('abc', $response->getBody(true)); + + // Try again with an unknown encoding + $response = new Response(200, array( + 'Content-MD5' => $hash, + 'Content-Encoding' => 'foobar' + ), 'abc'); + $request->dispatch('request.complete', array( + 'response' => $response + )); + + // Try again with compress + $body->compress('bzip2.compress'); + $response = new Response(200, array( + 'Content-MD5' => $body->getContentMd5(), + 'Content-Encoding' => 'compress' + ), 'abc'); + $request->dispatch('request.complete', array( + 'response' => $response + )); + + // Try again with encoding and disabled content-encoding checks + $request->getEventDispatcher()->removeSubscriber($plugin); + $plugin = new Md5ValidatorPlugin(false); + $request->getEventDispatcher()->addSubscriber($plugin); + $request->dispatch('request.complete', array( + 'response' => $response + )); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Mock/MockPluginTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Mock/MockPluginTest.php new file mode 100644 index 000000000..52be80609 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Mock/MockPluginTest.php @@ -0,0 +1,198 @@ +assertInternalType('array', MockPlugin::getSubscribedEvents()); + } + + public function testDescribesEvents() + { + $this->assertInternalType('array', MockPlugin::getAllEvents()); + } + + public function testCanBeTemporary() + { + $plugin = new MockPlugin(); + $this->assertFalse($plugin->isTemporary()); + $plugin = new MockPlugin(null, true); + $this->assertTrue($plugin->isTemporary()); + } + + public function testIsCountable() + { + $plugin = new MockPlugin(); + $plugin->addResponse(Response::fromMessage("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n")); + $this->assertEquals(1, count($plugin)); + } + + /** + * @depends testIsCountable + */ + public function testCanClearQueue() + { + $plugin = new MockPlugin(); + $plugin->addResponse(Response::fromMessage("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n")); + $plugin->clearQueue(); + $this->assertEquals(0, count($plugin)); + } + + public function testCanInspectQueue() + { + $plugin = new MockPlugin(); + $this->assertInternalType('array', $plugin->getQueue()); + $plugin->addResponse(Response::fromMessage("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n")); + $queue = $plugin->getQueue(); + $this->assertInternalType('array', $queue); + $this->assertEquals(1, count($queue)); + } + + public function testRetrievesResponsesFromFiles() + { + $response = MockPlugin::getMockFile(__DIR__ . '/../../TestData/mock_response'); + $this->assertInstanceOf('Guzzle\\Http\\Message\\Response', $response); + $this->assertEquals(200, $response->getStatusCode()); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testThrowsExceptionWhenResponseFileIsNotFound() + { + MockPlugin::getMockFile('missing/filename'); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testInvalidResponsesThrowAnException() + { + $p = new MockPlugin(); + $p->addResponse($this); + } + + public function testAddsResponseObjectsToQueue() + { + $p = new MockPlugin(); + $response = Response::fromMessage("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); + $p->addResponse($response); + $this->assertEquals(array($response), $p->getQueue()); + } + + public function testAddsResponseFilesToQueue() + { + $p = new MockPlugin(); + $p->addResponse(__DIR__ . '/../../TestData/mock_response'); + $this->assertEquals(1, count($p)); + } + + /** + * @depends testAddsResponseFilesToQueue + */ + public function testAddsMockResponseToRequestFromClient() + { + $p = new MockPlugin(); + $response = MockPlugin::getMockFile(__DIR__ . '/../../TestData/mock_response'); + $p->addResponse($response); + + $client = new Client('http://localhost:123/'); + $client->getEventDispatcher()->addSubscriber($p, 9999); + $request = $client->get(); + $request->send(); + + $this->assertSame($response, $request->getResponse()); + $this->assertEquals(0, count($p)); + } + + /** + * @depends testAddsResponseFilesToQueue + */ + public function testUpdateIgnoresWhenEmpty() + { + $p = new MockPlugin(); + $p->onRequestBeforeSend(new Event()); + } + + /** + * @depends testAddsMockResponseToRequestFromClient + */ + public function testDetachesTemporaryWhenEmpty() + { + $p = new MockPlugin(null, true); + $p->addResponse(MockPlugin::getMockFile(__DIR__ . '/../../TestData/mock_response')); + $client = new Client('http://localhost:123/'); + $client->getEventDispatcher()->addSubscriber($p, 9999); + $request = $client->get(); + $request->send(); + + $this->assertFalse($this->hasSubscriber($client, $p)); + } + + public function testLoadsResponsesFromConstructor() + { + $p = new MockPlugin(array(new Response(200))); + $this->assertEquals(1, $p->count()); + } + + public function testStoresMockedRequests() + { + $p = new MockPlugin(array(new Response(200), new Response(200))); + $client = new Client('http://localhost:123/'); + $client->getEventDispatcher()->addSubscriber($p, 9999); + + $request1 = $client->get(); + $request1->send(); + $this->assertEquals(array($request1), $p->getReceivedRequests()); + + $request2 = $client->get(); + $request2->send(); + $this->assertEquals(array($request1, $request2), $p->getReceivedRequests()); + + $p->flush(); + $this->assertEquals(array(), $p->getReceivedRequests()); + } + + public function testReadsBodiesFromMockedRequests() + { + $p = new MockPlugin(array(new Response(200))); + $p->readBodies(true); + $client = new Client('http://localhost:123/'); + $client->getEventDispatcher()->addSubscriber($p, 9999); + + $body = EntityBody::factory('foo'); + $request = $client->put(); + $request->setBody($body); + $request->send(); + $this->assertEquals(3, $body->ftell()); + } + + public function testCanMockBadRequestExceptions() + { + $client = new Client('http://localhost:123/'); + $ex = new CurlException('Foo'); + $mock = new MockPlugin(array($ex)); + $client->addSubscriber($mock); + $request = $client->get('foo'); + + try { + $request->send(); + $this->fail('Did not dequeue an exception'); + } catch (CurlException $e) { + $this->assertSame($e, $ex); + $this->assertSame($request, $ex->getRequest()); + } + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Oauth/OauthPluginTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Oauth/OauthPluginTest.php new file mode 100644 index 000000000..ed753177a --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Plugin/Oauth/OauthPluginTest.php @@ -0,0 +1,329 @@ + 'foo', + 'consumer_secret' => 'bar', + 'token' => 'count', + 'token_secret' => 'dracula' + ); + + protected function getRequest() + { + return RequestFactory::getInstance()->create('POST', 'http://www.test.com/path?a=b&c=d', null, array( + 'e' => 'f' + )); + } + + public function testSubscribesToEvents() + { + $events = OauthPlugin::getSubscribedEvents(); + $this->assertArrayHasKey('request.before_send', $events); + } + + public function testAcceptsConfigurationData() + { + $p = new OauthPlugin($this->config); + + // Access the config object + $class = new \ReflectionClass($p); + $property = $class->getProperty('config'); + $property->setAccessible(true); + $config = $property->getValue($p); + + $this->assertEquals('foo', $config['consumer_key']); + $this->assertEquals('bar', $config['consumer_secret']); + $this->assertEquals('count', $config['token']); + $this->assertEquals('dracula', $config['token_secret']); + $this->assertEquals('1.0', $config['version']); + $this->assertEquals('HMAC-SHA1', $config['signature_method']); + $this->assertEquals('header', $config['request_method']); + } + + public function testCreatesStringToSignFromPostRequest() + { + $p = new OauthPlugin($this->config); + $request = $this->getRequest(); + $signString = $p->getStringToSign($request, self::TIMESTAMP, self::NONCE); + + $this->assertContains('&e=f', rawurldecode($signString)); + + $expectedSignString = + // Method and URL + 'POST&http%3A%2F%2Fwww.test.com%2Fpath' . + // Sorted parameters from query string and body + '&a%3Db%26c%3Dd%26e%3Df%26oauth_consumer_key%3Dfoo' . + '%26oauth_nonce%3De7aa11195ca58349bec8b5ebe351d3497eb9e603%26' . + 'oauth_signature_method%3DHMAC-SHA1' . + '%26oauth_timestamp%3D' . self::TIMESTAMP . '%26oauth_token%3Dcount%26oauth_version%3D1.0'; + + $this->assertEquals($expectedSignString, $signString); + } + + public function testCreatesStringToSignIgnoringPostFields() + { + $config = $this->config; + $config['disable_post_params'] = true; + $p = new OauthPlugin($config); + $request = $this->getRequest(); + $sts = rawurldecode($p->getStringToSign($request, self::TIMESTAMP, self::NONCE)); + $this->assertNotContains('&e=f', $sts); + } + + public function testCreatesStringToSignFromPostRequestWithCustomContentType() + { + $p = new OauthPlugin($this->config); + $request = $this->getRequest(); + $request->setHeader('Content-Type', 'Foo'); + $this->assertEquals( + // Method and URL + 'POST&http%3A%2F%2Fwww.test.com%2Fpath' . + // Sorted parameters from query string and body + '&a%3Db%26c%3Dd%26oauth_consumer_key%3Dfoo' . + '%26oauth_nonce%3D'. self::NONCE .'%26' . + 'oauth_signature_method%3DHMAC-SHA1' . + '%26oauth_timestamp%3D' . self::TIMESTAMP . '%26oauth_token%3Dcount%26oauth_version%3D1.0', + $p->getStringToSign($request, self::TIMESTAMP, self::NONCE) + ); + } + + /** + * @depends testCreatesStringToSignFromPostRequest + */ + public function testConvertsBooleansToStrings() + { + $p = new OauthPlugin($this->config); + $request = $this->getRequest(); + $request->getQuery()->set('a', true); + $request->getQuery()->set('c', false); + $this->assertContains('&a%3Dtrue%26c%3Dfalse', $p->getStringToSign($request, self::TIMESTAMP, self::NONCE)); + } + + public function testCreatesStringToSignFromPostRequestWithNullValues() + { + $config = array( + 'consumer_key' => 'foo', + 'consumer_secret' => 'bar', + 'token' => null, + 'token_secret' => 'dracula' + ); + + $p = new OauthPlugin($config); + $request = $this->getRequest(); + $signString = $p->getStringToSign($request, self::TIMESTAMP, self::NONCE); + + $this->assertContains('&e=f', rawurldecode($signString)); + + $expectedSignString = // Method and URL + 'POST&http%3A%2F%2Fwww.test.com%2Fpath' . + // Sorted parameters from query string and body + '&a%3Db%26c%3Dd%26e%3Df%26oauth_consumer_key%3Dfoo' . + '%26oauth_nonce%3De7aa11195ca58349bec8b5ebe351d3497eb9e603%26' . + 'oauth_signature_method%3DHMAC-SHA1' . + '%26oauth_timestamp%3D' . self::TIMESTAMP . '%26oauth_version%3D1.0'; + + $this->assertEquals($expectedSignString, $signString); + } + + /** + * @depends testCreatesStringToSignFromPostRequest + */ + public function testMultiDimensionalArray() + { + $p = new OauthPlugin($this->config); + $request = $this->getRequest(); + $request->getQuery()->set('a', array('b' => array('e' => 'f', 'c' => 'd'))); + $this->assertContains('a%255Bb%255D%255Bc%255D%3Dd%26a%255Bb%255D%255Be%255D%3Df%26c%3Dd%26e%3Df%26', $p->getStringToSign($request, self::TIMESTAMP, self::NONCE)); + } + + /** + * @depends testCreatesStringToSignFromPostRequest + */ + public function testSignsStrings() + { + $p = new OauthPlugin(array_merge($this->config, array( + 'signature_callback' => function($string, $key) { + return "_{$string}|{$key}_"; + } + ))); + $request = $this->getRequest(); + $sig = $p->getSignature($request, self::TIMESTAMP, self::NONCE); + $this->assertEquals( + '_POST&http%3A%2F%2Fwww.test.com%2Fpath&a%3Db%26c%3Dd%26e%3Df%26oauth_consumer_key%3Dfoo' . + '%26oauth_nonce%3D'. self::NONCE .'%26oauth_signature_method%3DHMAC-SHA1' . + '%26oauth_timestamp%3D' . self::TIMESTAMP . '%26oauth_token%3Dcount%26oauth_version%3D1.0|' . + 'bar&dracula_', + base64_decode($sig) + ); + } + + /** + * Test that the Oauth is signed correctly and that extra strings haven't been added + * to the authorization header. + */ + public function testSignsOauthRequests() + { + $p = new OauthPlugin($this->config); + $event = new Event(array( + 'request' => $this->getRequest(), + 'timestamp' => self::TIMESTAMP + )); + $params = $p->onRequestBeforeSend($event); + + $this->assertTrue($event['request']->hasHeader('Authorization')); + + $authorizationHeader = (string)$event['request']->getHeader('Authorization'); + + $this->assertStringStartsWith('OAuth ', $authorizationHeader); + + $stringsToCheck = array( + 'oauth_consumer_key="foo"', + 'oauth_nonce="'.urlencode($params['oauth_nonce']).'"', + 'oauth_signature="'.urlencode($params['oauth_signature']).'"', + 'oauth_signature_method="HMAC-SHA1"', + 'oauth_timestamp="' . self::TIMESTAMP . '"', + 'oauth_token="count"', + 'oauth_version="1.0"', + ); + + $totalLength = strlen('OAuth '); + + //Separator is not used before first parameter. + $separator = ''; + + foreach ($stringsToCheck as $stringToCheck) { + $this->assertContains($stringToCheck, $authorizationHeader); + $totalLength += strlen($separator); + $totalLength += strlen($stringToCheck); + $separator = ', '; + } + + // Technically this test is not universally valid. It would be allowable to have extra \n characters + // in the Authorization header. However Guzzle does not do this, so we just perform a simple check + // on length to validate the Authorization header is composed of only the strings above. + $this->assertEquals($totalLength, strlen($authorizationHeader), 'Authorization has extra characters i.e. contains extra elements compared to stringsToCheck.'); + } + + public function testSignsOauthQueryStringRequest() + { + $config = array_merge( + $this->config, + array('request_method' => OauthPlugin::REQUEST_METHOD_QUERY) + ); + + $p = new OauthPlugin($config); + $event = new Event(array( + 'request' => $this->getRequest(), + 'timestamp' => self::TIMESTAMP + )); + $params = $p->onRequestBeforeSend($event); + + $this->assertFalse($event['request']->hasHeader('Authorization')); + + $stringsToCheck = array( + 'a=b', + 'c=d', + 'oauth_consumer_key=foo', + 'oauth_nonce='.urlencode($params['oauth_nonce']), + 'oauth_signature='.urlencode($params['oauth_signature']), + 'oauth_signature_method=HMAC-SHA1', + 'oauth_timestamp='.self::TIMESTAMP, + 'oauth_token=count', + 'oauth_version=1.0', + ); + + $queryString = (string) $event['request']->getQuery(); + + $totalLength = strlen('?'); + + //Separator is not used before first parameter. + $separator = ''; + + foreach ($stringsToCheck as $stringToCheck) { + $this->assertContains($stringToCheck, $queryString); + $totalLength += strlen($separator); + $totalLength += strlen($stringToCheck); + $separator = '&'; + } + + // Removes the last query string separator '&' + $totalLength -= 1; + + $this->assertEquals($totalLength, strlen($queryString), 'Query string has extra characters i.e. contains extra elements compared to stringsToCheck.'); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testInvalidArgumentExceptionOnMethodError() + { + $config = array_merge( + $this->config, + array('request_method' => 'FakeMethod') + ); + + $p = new OauthPlugin($config); + $event = new Event(array( + 'request' => $this->getRequest(), + 'timestamp' => self::TIMESTAMP + )); + + $p->onRequestBeforeSend($event); + } + + public function testDoesNotAddFalseyValuesToAuthorization() + { + unset($this->config['token']); + $p = new OauthPlugin($this->config); + $event = new Event(array('request' => $this->getRequest(), 'timestamp' => self::TIMESTAMP)); + $p->onRequestBeforeSend($event); + $this->assertTrue($event['request']->hasHeader('Authorization')); + $this->assertNotContains('oauth_token=', (string) $event['request']->getHeader('Authorization')); + } + + public function testOptionalOauthParametersAreNotAutomaticallyAdded() + { + // The only required Oauth parameters are the consumer key and secret. That is enough credentials + // for signing oauth requests. + $config = array( + 'consumer_key' => 'foo', + 'consumer_secret' => 'bar', + ); + + $plugin = new OauthPlugin($config); + $event = new Event(array( + 'request' => $this->getRequest(), + 'timestamp' => self::TIMESTAMP + )); + + $timestamp = $plugin->getTimestamp($event); + $request = $event['request']; + $nonce = $plugin->generateNonce($request); + + $paramsToSign = $plugin->getParamsToSign($request, $timestamp, $nonce); + + $optionalParams = array( + 'callback' => 'oauth_callback', + 'token' => 'oauth_token', + 'verifier' => 'oauth_verifier', + 'token_secret' => 'token_secret' + ); + + foreach ($optionalParams as $optionName => $oauthName) { + $this->assertArrayNotHasKey($oauthName, $paramsToSign, "Optional Oauth param '$oauthName' was not set via config variable '$optionName', but it is listed in getParamsToSign()."); + } + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Service/AbstractConfigLoaderTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Service/AbstractConfigLoaderTest.php new file mode 100644 index 000000000..8b42fb839 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Service/AbstractConfigLoaderTest.php @@ -0,0 +1,149 @@ +loader = $this->getMockBuilder('Guzzle\Service\AbstractConfigLoader') + ->setMethods(array('build')) + ->getMockForAbstractClass(); + } + + public function tearDown() + { + foreach ($this->cleanup as $file) { + unlink($file); + } + } + + /** + * @expectedException \Guzzle\Common\Exception\InvalidArgumentException + */ + public function testOnlyLoadsSupportedTypes() + { + $this->loader->load(new \stdClass()); + } + + /** + * @expectedException \Guzzle\Common\Exception\InvalidArgumentException + * @expectedExceptionMessage Unable to open fooooooo.json + */ + public function testFileMustBeReadable() + { + $this->loader->load('fooooooo.json'); + } + + /** + * @expectedException \Guzzle\Common\Exception\InvalidArgumentException + * @expectedExceptionMessage Unknown file extension + */ + public function testMustBeSupportedExtension() + { + $this->loader->load(dirname(__DIR__) . '/TestData/FileBody.txt'); + } + + /** + * @expectedException \Guzzle\Common\Exception\RuntimeException + * @expectedExceptionMessage Error loading JSON data from + */ + public function testJsonMustBeValue() + { + $filename = tempnam(sys_get_temp_dir(), 'json') . '.json'; + file_put_contents($filename, '{/{./{}foo'); + $this->cleanup[] = $filename; + $this->loader->load($filename); + } + + /** + * @expectedException \Guzzle\Common\Exception\InvalidArgumentException + * @expectedExceptionMessage PHP files must return an array + */ + public function testPhpFilesMustReturnAnArray() + { + $filename = tempnam(sys_get_temp_dir(), 'php') . '.php'; + file_put_contents($filename, 'cleanup[] = $filename; + $this->loader->load($filename); + } + + public function testLoadsPhpFileIncludes() + { + $filename = tempnam(sys_get_temp_dir(), 'php') . '.php'; + file_put_contents($filename, ' "bar");'); + $this->cleanup[] = $filename; + $this->loader->expects($this->exactly(1))->method('build')->will($this->returnArgument(0)); + $config = $this->loader->load($filename); + $this->assertEquals(array('foo' => 'bar'), $config); + } + + public function testCanCreateFromJson() + { + $file = dirname(__DIR__) . '/TestData/services/json1.json'; + // The build method will just return the config data + $this->loader->expects($this->exactly(1))->method('build')->will($this->returnArgument(0)); + $data = $this->loader->load($file); + // Ensure that the config files were merged using the includes directives + $this->assertArrayHasKey('includes', $data); + $this->assertArrayHasKey('services', $data); + $this->assertInternalType('array', $data['services']['foo']); + $this->assertInternalType('array', $data['services']['abstract']); + $this->assertInternalType('array', $data['services']['mock']); + $this->assertEquals('bar', $data['services']['foo']['params']['baz']); + } + + public function testUsesAliases() + { + $file = dirname(__DIR__) . '/TestData/services/json1.json'; + $this->loader->addAlias('foo', $file); + // The build method will just return the config data + $this->loader->expects($this->exactly(1))->method('build')->will($this->returnArgument(0)); + $data = $this->loader->load('foo'); + $this->assertEquals('bar', $data['services']['foo']['params']['baz']); + } + + /** + * @expectedException \Guzzle\Common\Exception\InvalidArgumentException + * @expectedExceptionMessage Unable to open foo.json + */ + public function testCanRemoveAliases() + { + $file = dirname(__DIR__) . '/TestData/services/json1.json'; + $this->loader->addAlias('foo.json', $file); + $this->loader->removeAlias('foo.json'); + $this->loader->load('foo.json'); + } + + public function testCanLoadArraysWithIncludes() + { + $file = dirname(__DIR__) . '/TestData/services/json1.json'; + $config = array('includes' => array($file)); + // The build method will just return the config data + $this->loader->expects($this->exactly(1))->method('build')->will($this->returnArgument(0)); + $data = $this->loader->load($config); + $this->assertEquals('bar', $data['services']['foo']['params']['baz']); + } + + public function testDoesNotEnterInfiniteLoop() + { + $prefix = $file = dirname(__DIR__) . '/TestData/description'; + $this->loader->load("{$prefix}/baz.json"); + $this->assertCount(4, $this->readAttribute($this->loader, 'loadedFiles')); + // Ensure that the internal list of loaded files is reset + $this->loader->load("{$prefix}/../test_service2.json"); + $this->assertCount(1, $this->readAttribute($this->loader, 'loadedFiles')); + // Ensure that previously loaded files will be reloaded when starting fresh + $this->loader->load("{$prefix}/baz.json"); + $this->assertCount(4, $this->readAttribute($this->loader, 'loadedFiles')); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Service/Builder/ServiceBuilderLoaderTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Service/Builder/ServiceBuilderLoaderTest.php new file mode 100644 index 000000000..f63070e38 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Service/Builder/ServiceBuilderLoaderTest.php @@ -0,0 +1,177 @@ + array( + 'abstract' => array( + 'params' => array( + 'access_key' => 'xyz', + 'secret' => 'abc', + ), + ), + 'foo' => array( + 'extends' => 'abstract', + 'params' => array( + 'baz' => 'bar', + ), + ), + 'mock' => array( + 'extends' => 'abstract', + 'params' => array( + 'username' => 'foo', + 'password' => 'baz', + 'subdomain' => 'bar', + ) + ) + ) + ); + + $builder = $arrayFactory->load($data); + + // Ensure that services were parsed + $this->assertTrue(isset($builder['mock'])); + $this->assertTrue(isset($builder['abstract'])); + $this->assertTrue(isset($builder['foo'])); + $this->assertFalse(isset($builder['jimmy'])); + } + + /** + * @expectedException Guzzle\Service\Exception\ServiceNotFoundException + * @expectedExceptionMessage foo is trying to extend a non-existent service: abstract + */ + public function testThrowsExceptionWhenExtendingNonExistentService() + { + $arrayFactory = new ServiceBuilderLoader(); + + $data = array( + 'services' => array( + 'foo' => array( + 'extends' => 'abstract' + ) + ) + ); + + $builder = $arrayFactory->load($data); + } + + public function testAllowsGlobalParameterOverrides() + { + $arrayFactory = new ServiceBuilderLoader(); + + $data = array( + 'services' => array( + 'foo' => array( + 'params' => array( + 'foo' => 'baz', + 'bar' => 'boo' + ) + ) + ) + ); + + $builder = $arrayFactory->load($data, array( + 'bar' => 'jar', + 'far' => 'car' + )); + + $compiled = json_decode($builder->serialize(), true); + $this->assertEquals(array( + 'foo' => 'baz', + 'bar' => 'jar', + 'far' => 'car' + ), $compiled['foo']['params']); + } + + public function tstDoesNotErrorOnCircularReferences() + { + $arrayFactory = new ServiceBuilderLoader(); + $arrayFactory->load(array( + 'services' => array( + 'too' => array('extends' => 'ball'), + 'ball' => array('extends' => 'too'), + ) + )); + } + + public function configProvider() + { + $foo = array( + 'extends' => 'bar', + 'class' => 'stdClass', + 'params' => array('a' => 'test', 'b' => '456') + ); + + return array( + array( + // Does not extend the existing `foo` service but overwrites it + array( + 'services' => array( + 'foo' => $foo, + 'bar' => array('params' => array('baz' => '123')) + ) + ), + array( + 'services' => array( + 'foo' => array('class' => 'Baz') + ) + ), + array( + 'services' => array( + 'foo' => array('class' => 'Baz'), + 'bar' => array('params' => array('baz' => '123')) + ) + ) + ), + array( + // Extends the existing `foo` service + array( + 'services' => array( + 'foo' => $foo, + 'bar' => array('params' => array('baz' => '123')) + ) + ), + array( + 'services' => array( + 'foo' => array( + 'extends' => 'foo', + 'params' => array('b' => '123', 'c' => 'def') + ) + ) + ), + array( + 'services' => array( + 'foo' => array( + 'extends' => 'bar', + 'class' => 'stdClass', + 'params' => array('a' => 'test', 'b' => '123', 'c' => 'def') + ), + 'bar' => array('params' => array('baz' => '123')) + ) + ) + ) + ); + } + + /** + * @dataProvider configProvider + */ + public function testCombinesConfigs($a, $b, $c) + { + $l = new ServiceBuilderLoader(); + $m = new \ReflectionMethod($l, 'mergeData'); + $m->setAccessible(true); + $this->assertEquals($c, $m->invoke($l, $a, $b)); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Service/Builder/ServiceBuilderTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Service/Builder/ServiceBuilderTest.php new file mode 100644 index 000000000..e1b3a1d49 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Service/Builder/ServiceBuilderTest.php @@ -0,0 +1,317 @@ + array( + 'class' => 'Guzzle\Tests\Service\Mock\MockClient', + 'params' => array( + 'username' => 'michael', + 'password' => 'testing123', + 'subdomain' => 'michael', + ), + ), + 'billy.mock' => array( + 'alias' => 'Hello!', + 'class' => 'Guzzle\Tests\Service\Mock\MockClient', + 'params' => array( + 'username' => 'billy', + 'password' => 'passw0rd', + 'subdomain' => 'billy', + ), + ), + 'billy.testing' => array( + 'extends' => 'billy.mock', + 'params' => array( + 'subdomain' => 'test.billy', + ), + ), + 'missing_params' => array( + 'extends' => 'billy.mock' + ) + ); + + public function testAllowsSerialization() + { + $builder = ServiceBuilder::factory($this->arrayData); + $cached = unserialize(serialize($builder)); + $this->assertEquals($cached, $builder); + } + + public function testDelegatesFactoryMethodToAbstractFactory() + { + $builder = ServiceBuilder::factory($this->arrayData); + $c = $builder->get('michael.mock'); + $this->assertInstanceOf('Guzzle\Tests\Service\Mock\MockClient', $c); + } + + /** + * @expectedException Guzzle\Service\Exception\ServiceNotFoundException + * @expectedExceptionMessage No service is registered as foobar + */ + public function testThrowsExceptionWhenGettingInvalidClient() + { + ServiceBuilder::factory($this->arrayData)->get('foobar'); + } + + public function testStoresClientCopy() + { + $builder = ServiceBuilder::factory($this->arrayData); + $client = $builder->get('michael.mock'); + $this->assertInstanceOf('Guzzle\Tests\Service\Mock\MockClient', $client); + $this->assertEquals('http://127.0.0.1:8124/v1/michael', $client->getBaseUrl()); + $this->assertEquals($client, $builder->get('michael.mock')); + + // Get another client but throw this one away + $client2 = $builder->get('billy.mock', true); + $this->assertInstanceOf('Guzzle\Tests\Service\Mock\MockClient', $client2); + $this->assertEquals('http://127.0.0.1:8124/v1/billy', $client2->getBaseUrl()); + + // Make sure the original client is still there and set + $this->assertTrue($client === $builder->get('michael.mock')); + + // Create a new billy.mock client that is stored + $client3 = $builder->get('billy.mock'); + + // Make sure that the stored billy.mock client is equal to the other stored client + $this->assertTrue($client3 === $builder->get('billy.mock')); + + // Make sure that this client is not equal to the previous throwaway client + $this->assertFalse($client2 === $builder->get('billy.mock')); + } + + public function testBuildersPassOptionsThroughToClients() + { + $s = new ServiceBuilder(array( + 'michael.mock' => array( + 'class' => 'Guzzle\Tests\Service\Mock\MockClient', + 'params' => array( + 'base_url' => 'http://www.test.com/', + 'subdomain' => 'michael', + 'password' => 'test', + 'username' => 'michael', + 'curl.curlopt_proxyport' => 8080 + ) + ) + )); + + $c = $s->get('michael.mock'); + $this->assertEquals(8080, $c->getConfig('curl.curlopt_proxyport')); + } + + public function testUsesTheDefaultBuilderWhenNoBuilderIsSpecified() + { + $s = new ServiceBuilder(array( + 'michael.mock' => array( + 'class' => 'Guzzle\Tests\Service\Mock\MockClient', + 'params' => array( + 'base_url' => 'http://www.test.com/', + 'subdomain' => 'michael', + 'password' => 'test', + 'username' => 'michael', + 'curl.curlopt_proxyport' => 8080 + ) + ) + )); + + $c = $s->get('michael.mock'); + $this->assertInstanceOf('Guzzle\Tests\Service\Mock\MockClient', $c); + } + + public function testUsedAsArray() + { + $b = ServiceBuilder::factory($this->arrayData); + $this->assertTrue($b->offsetExists('michael.mock')); + $this->assertFalse($b->offsetExists('not_there')); + $this->assertInstanceOf('Guzzle\Service\Client', $b['michael.mock']); + + unset($b['michael.mock']); + $this->assertFalse($b->offsetExists('michael.mock')); + + $b['michael.mock'] = new Client('http://www.test.com/'); + $this->assertInstanceOf('Guzzle\Service\Client', $b['michael.mock']); + } + + public function testFactoryCanCreateFromJson() + { + $tmp = sys_get_temp_dir() . '/test.js'; + file_put_contents($tmp, json_encode($this->arrayData)); + $b = ServiceBuilder::factory($tmp); + unlink($tmp); + $s = $b->get('billy.testing'); + $this->assertEquals('test.billy', $s->getConfig('subdomain')); + $this->assertEquals('billy', $s->getConfig('username')); + } + + public function testFactoryCanCreateFromArray() + { + $b = ServiceBuilder::factory($this->arrayData); + $s = $b->get('billy.testing'); + $this->assertEquals('test.billy', $s->getConfig('subdomain')); + $this->assertEquals('billy', $s->getConfig('username')); + } + + public function testFactoryDoesNotRequireParams() + { + $b = ServiceBuilder::factory($this->arrayData); + $s = $b->get('missing_params'); + $this->assertEquals('billy', $s->getConfig('username')); + } + + public function testBuilderAllowsReferencesBetweenClients() + { + $builder = ServiceBuilder::factory(array( + 'a' => array( + 'class' => 'Guzzle\Tests\Service\Mock\MockClient', + 'params' => array( + 'other_client' => '{b}', + 'username' => 'x', + 'password' => 'y', + 'subdomain' => 'z' + ) + ), + 'b' => array( + 'class' => 'Guzzle\Tests\Service\Mock\MockClient', + 'params' => array( + 'username' => '1', + 'password' => '2', + 'subdomain' => '3' + ) + ) + )); + + $client = $builder['a']; + $this->assertEquals('x', $client->getConfig('username')); + $this->assertSame($builder['b'], $client->getConfig('other_client')); + $this->assertEquals('1', $builder['b']->getConfig('username')); + } + + public function testEmitsEventsWhenClientsAreCreated() + { + // Ensure that the client signals that it emits an event + $this->assertEquals(array('service_builder.create_client'), ServiceBuilder::getAllEvents()); + + // Create a test service builder + $builder = ServiceBuilder::factory(array( + 'a' => array( + 'class' => 'Guzzle\Tests\Service\Mock\MockClient', + 'params' => array( + 'username' => 'test', + 'password' => '123', + 'subdomain' => 'z' + ) + ) + )); + + // Add an event listener to pick up client creation events + $emits = 0; + $builder->getEventDispatcher()->addListener('service_builder.create_client', function($event) use (&$emits) { + $emits++; + }); + + // Get the 'a' client by name + $client = $builder->get('a'); + + // Ensure that the event was emitted once, and that the client was present + $this->assertEquals(1, $emits); + $this->assertInstanceOf('Guzzle\Tests\Service\Mock\MockClient', $client); + } + + public function testCanAddGlobalParametersToServicesOnLoad() + { + $builder = ServiceBuilder::factory($this->arrayData, array( + 'username' => 'fred', + 'new_value' => 'test' + )); + + $data = json_decode($builder->serialize(), true); + + foreach ($data as $service) { + $this->assertEquals('fred', $service['params']['username']); + $this->assertEquals('test', $service['params']['new_value']); + } + } + + public function testAddsGlobalPlugins() + { + $b = new ServiceBuilder($this->arrayData); + $b->addGlobalPlugin(new HistoryPlugin()); + $s = $b->get('michael.mock'); + $this->assertTrue($s->getEventDispatcher()->hasListeners('request.sent')); + } + + public function testCanGetData() + { + $b = new ServiceBuilder($this->arrayData); + $this->assertEquals($this->arrayData['michael.mock'], $b->getData('michael.mock')); + $this->assertNull($b->getData('ewofweoweofe')); + } + + public function testCanGetByAlias() + { + $b = new ServiceBuilder($this->arrayData); + $this->assertSame($b->get('billy.mock'), $b->get('Hello!')); + } + + public function testCanOverwriteParametersForThrowawayClients() + { + $b = new ServiceBuilder($this->arrayData); + + $c1 = $b->get('michael.mock'); + $this->assertEquals('michael', $c1->getConfig('username')); + + $c2 = $b->get('michael.mock', array('username' => 'jeremy')); + $this->assertEquals('jeremy', $c2->getConfig('username')); + } + + public function testGettingAThrowawayClientWithParametersDoesNotAffectGettingOtherClients() + { + $b = new ServiceBuilder($this->arrayData); + + $c1 = $b->get('michael.mock', array('username' => 'jeremy')); + $this->assertEquals('jeremy', $c1->getConfig('username')); + + $c2 = $b->get('michael.mock'); + $this->assertEquals('michael', $c2->getConfig('username')); + } + + public function testCanUseArbitraryData() + { + $b = new ServiceBuilder(); + $b['a'] = 'foo'; + $this->assertTrue(isset($b['a'])); + $this->assertEquals('foo', $b['a']); + unset($b['a']); + $this->assertFalse(isset($b['a'])); + } + + public function testCanRegisterServiceData() + { + $b = new ServiceBuilder(); + $b['a'] = array( + 'class' => 'Guzzle\Tests\Service\Mock\MockClient', + 'params' => array( + 'username' => 'billy', + 'password' => 'passw0rd', + 'subdomain' => 'billy', + ) + ); + $this->assertTrue(isset($b['a'])); + $this->assertInstanceOf('Guzzle\Tests\Service\Mock\MockClient', $b['a']); + $client = $b['a']; + unset($b['a']); + $this->assertFalse(isset($b['a'])); + // Ensure that instantiated clients can be registered + $b['mock'] = $client; + $this->assertSame($client, $b['mock']); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Service/CachingConfigLoaderTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Service/CachingConfigLoaderTest.php new file mode 100644 index 000000000..b8245ad58 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Service/CachingConfigLoaderTest.php @@ -0,0 +1,43 @@ +getMockBuilder('Guzzle\Service\ConfigLoaderInterface') + ->setMethods(array('load')) + ->getMockForAbstractClass(); + $data = array('foo' => 'bar'); + $loader->expects($this->once()) + ->method('load') + ->will($this->returnValue($data)); + $cache = new CachingConfigLoader($loader, $cache); + $this->assertEquals($data, $cache->load('foo')); + $this->assertEquals($data, $cache->load('foo')); + } + + public function testDoesNotCacheArrays() + { + $cache = new DoctrineCacheAdapter(new ArrayCache()); + $loader = $this->getMockBuilder('Guzzle\Service\ConfigLoaderInterface') + ->setMethods(array('load')) + ->getMockForAbstractClass(); + $data = array('foo' => 'bar'); + $loader->expects($this->exactly(2)) + ->method('load') + ->will($this->returnValue($data)); + $cache = new CachingConfigLoader($loader, $cache); + $this->assertEquals($data, $cache->load(array())); + $this->assertEquals($data, $cache->load(array())); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Service/ClientTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Service/ClientTest.php new file mode 100644 index 000000000..6ca92cd13 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Service/ClientTest.php @@ -0,0 +1,307 @@ +serviceTest = new ServiceDescription(array( + 'test_command' => new Operation(array( + 'doc' => 'documentationForCommand', + 'method' => 'DELETE', + 'class' => 'Guzzle\\Tests\\Service\\Mock\\Command\\MockCommand', + 'args' => array( + 'bucket' => array( + 'required' => true + ), + 'key' => array( + 'required' => true + ) + ) + )) + )); + + $this->service = ServiceDescription::factory(__DIR__ . '/../TestData/test_service.json'); + } + + public function testAllowsCustomClientParameters() + { + $client = new Mock\MockClient(null, array( + Client::COMMAND_PARAMS => array(AbstractCommand::RESPONSE_PROCESSING => 'foo') + )); + $command = $client->getCommand('mock_command'); + $this->assertEquals('foo', $command->get(AbstractCommand::RESPONSE_PROCESSING)); + } + + public function testFactoryCreatesClient() + { + $client = Client::factory(array( + 'base_url' => 'http://www.test.com/', + 'test' => '123' + )); + + $this->assertEquals('http://www.test.com/', $client->getBaseUrl()); + $this->assertEquals('123', $client->getConfig('test')); + } + + public function testFactoryDoesNotRequireBaseUrl() + { + $client = Client::factory(); + } + + public function testDescribesEvents() + { + $this->assertInternalType('array', Client::getAllEvents()); + } + + public function testExecutesCommands() + { + $this->getServer()->flush(); + $this->getServer()->enqueue("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); + + $client = new Client($this->getServer()->getUrl()); + $cmd = new MockCommand(); + $client->execute($cmd); + + $this->assertInstanceOf('Guzzle\\Http\\Message\\Response', $cmd->getResponse()); + $this->assertInstanceOf('Guzzle\\Http\\Message\\Response', $cmd->getResult()); + $this->assertEquals(1, count($this->getServer()->getReceivedRequests(false))); + } + + public function testExecutesCommandsWithArray() + { + $client = new Client('http://www.test.com/'); + $client->getEventDispatcher()->addSubscriber(new MockPlugin(array( + new Response(200), + new Response(200) + ))); + + // Create a command set and a command + $set = array(new MockCommand(), new MockCommand()); + $client->execute($set); + + // Make sure it sent + $this->assertTrue($set[0]->isExecuted()); + $this->assertTrue($set[1]->isExecuted()); + } + + /** + * @expectedException Guzzle\Common\Exception\InvalidArgumentException + */ + public function testThrowsExceptionWhenInvalidCommandIsExecuted() + { + $client = new Client(); + $client->execute(new \stdClass()); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testThrowsExceptionWhenMissingCommand() + { + $client = new Client(); + + $mock = $this->getMock('Guzzle\\Service\\Command\\Factory\\FactoryInterface'); + $mock->expects($this->any()) + ->method('factory') + ->with($this->equalTo('test')) + ->will($this->returnValue(null)); + + $client->setCommandFactory($mock); + $client->getCommand('test'); + } + + public function testCreatesCommandsUsingCommandFactory() + { + $mockCommand = new MockCommand(); + + $client = new Mock\MockClient(); + $mock = $this->getMock('Guzzle\\Service\\Command\\Factory\\FactoryInterface'); + $mock->expects($this->any()) + ->method('factory') + ->with($this->equalTo('foo')) + ->will($this->returnValue($mockCommand)); + + $client->setCommandFactory($mock); + + $command = $client->getCommand('foo', array('acl' => '123')); + $this->assertSame($mockCommand, $command); + $command = $client->getCommand('foo', array('acl' => '123')); + $this->assertSame($mockCommand, $command); + $this->assertSame($client, $command->getClient()); + } + + public function testOwnsServiceDescription() + { + $client = new Mock\MockClient(); + $this->assertNull($client->getDescription()); + + $description = $this->getMock('Guzzle\\Service\\Description\\ServiceDescription'); + $this->assertSame($client, $client->setDescription($description)); + $this->assertSame($description, $client->getDescription()); + } + + public function testOwnsResourceIteratorFactory() + { + $client = new Mock\MockClient(); + + $method = new \ReflectionMethod($client, 'getResourceIteratorFactory'); + $method->setAccessible(TRUE); + $rf1 = $method->invoke($client); + + $rf = $this->readAttribute($client, 'resourceIteratorFactory'); + $this->assertInstanceOf('Guzzle\\Service\\Resource\\ResourceIteratorClassFactory', $rf); + $this->assertSame($rf1, $rf); + + $rf = new ResourceIteratorClassFactory('Guzzle\Tests\Service\Mock'); + $client->setResourceIteratorFactory($rf); + $this->assertNotSame($rf1, $rf); + } + + public function testClientResetsRequestsBeforeExecutingCommands() + { + $this->getServer()->flush(); + $this->getServer()->enqueue(array( + "HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nHi", + "HTTP/1.1 200 OK\r\nContent-Length: 1\r\n\r\nI" + )); + + $client = new Mock\MockClient($this->getServer()->getUrl()); + + $command = $client->getCommand('mock_command'); + $client->execute($command); + $client->execute($command); + $this->assertEquals('I', $command->getResponse()->getBody(true)); + } + + public function testClientCreatesIterators() + { + $client = new Mock\MockClient(); + + $iterator = $client->getIterator('mock_command', array( + 'foo' => 'bar' + ), array( + 'limit' => 10 + )); + + $this->assertInstanceOf('Guzzle\Tests\Service\Mock\Model\MockCommandIterator', $iterator); + $this->assertEquals(10, $this->readAttribute($iterator, 'limit')); + + $command = $this->readAttribute($iterator, 'originalCommand'); + $this->assertEquals('bar', $command->get('foo')); + } + + public function testClientCreatesIteratorsWithNoOptions() + { + $client = new Mock\MockClient(); + $iterator = $client->getIterator('mock_command'); + $this->assertInstanceOf('Guzzle\Tests\Service\Mock\Model\MockCommandIterator', $iterator); + } + + public function testClientCreatesIteratorsWithCommands() + { + $client = new Mock\MockClient(); + $command = new MockCommand(); + $iterator = $client->getIterator($command); + $this->assertInstanceOf('Guzzle\Tests\Service\Mock\Model\MockCommandIterator', $iterator); + $iteratorCommand = $this->readAttribute($iterator, 'originalCommand'); + $this->assertSame($command, $iteratorCommand); + } + + public function testClientHoldsInflector() + { + $client = new Mock\MockClient(); + $this->assertInstanceOf('Guzzle\Inflection\MemoizingInflector', $client->getInflector()); + + $inflector = new Inflector(); + $client->setInflector($inflector); + $this->assertSame($inflector, $client->getInflector()); + } + + public function testClientAddsGlobalCommandOptions() + { + $client = new Mock\MockClient('http://www.foo.com', array( + Client::COMMAND_PARAMS => array( + 'mesa' => 'bar' + ) + )); + $command = $client->getCommand('mock_command'); + $this->assertEquals('bar', $command->get('mesa')); + } + + public function testSupportsServiceDescriptionBaseUrls() + { + $description = new ServiceDescription(array('baseUrl' => 'http://foo.com')); + $client = new Client(); + $client->setDescription($description); + $this->assertEquals('http://foo.com', $client->getBaseUrl()); + } + + public function testMergesDefaultCommandParamsCorrectly() + { + $client = new Mock\MockClient('http://www.foo.com', array( + Client::COMMAND_PARAMS => array( + 'mesa' => 'bar', + 'jar' => 'jar' + ) + )); + $command = $client->getCommand('mock_command', array('jar' => 'test')); + $this->assertEquals('bar', $command->get('mesa')); + $this->assertEquals('test', $command->get('jar')); + } + + /** + * @expectedException \Guzzle\Http\Exception\BadResponseException + */ + public function testWrapsSingleCommandExceptions() + { + $client = new Mock\MockClient('http://foobaz.com'); + $mock = new MockPlugin(array(new Response(401))); + $client->addSubscriber($mock); + $client->execute(new MockCommand()); + } + + public function testWrapsMultipleCommandExceptions() + { + $client = new Mock\MockClient('http://foobaz.com'); + $mock = new MockPlugin(array(new Response(200), new Response(200), new Response(404), new Response(500))); + $client->addSubscriber($mock); + + $cmds = array(new MockCommand(), new MockCommand(), new MockCommand(), new MockCommand()); + try { + $client->execute($cmds); + } catch (CommandTransferException $e) { + $this->assertEquals(2, count($e->getFailedRequests())); + $this->assertEquals(2, count($e->getSuccessfulRequests())); + $this->assertEquals(2, count($e->getFailedCommands())); + $this->assertEquals(2, count($e->getSuccessfulCommands())); + + foreach ($e->getSuccessfulCommands() as $c) { + $this->assertTrue($c->getResponse()->isSuccessful()); + } + + foreach ($e->getFailedCommands() as $c) { + $this->assertFalse($c->getRequest()->getResponse()->isSuccessful()); + } + } + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/AbstractCommandTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/AbstractCommandTest.php new file mode 100644 index 000000000..1004fae66 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/AbstractCommandTest.php @@ -0,0 +1,16 @@ +setDescription(ServiceDescription::factory(__DIR__ . '/../../TestData/test_service.json')); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/ClosureCommandTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/ClosureCommandTest.php new file mode 100644 index 000000000..d76224697 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/ClosureCommandTest.php @@ -0,0 +1,54 @@ + function($command, $api) { + $command->set('testing', '123'); + $request = RequestFactory::getInstance()->create('GET', 'http://www.test.com/'); + return $request; + } + )); + + $client = $this->getServiceBuilder()->get('mock'); + $c->setClient($client)->prepare(); + $this->assertEquals('123', $c->get('testing')); + $this->assertEquals('http://www.test.com/', $c->getRequest()->getUrl()); + } + + /** + * @expectedException UnexpectedValueException + * @expectedExceptionMessage Closure command did not return a RequestInterface object + */ + public function testMustReturnRequest() + { + $c = new ClosureCommand(array( + 'closure' => function($command, $api) { + return false; + } + )); + + $client = $this->getServiceBuilder()->get('mock'); + $c->setClient($client)->prepare(); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/CommandTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/CommandTest.php new file mode 100644 index 000000000..1b95ac3b8 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/CommandTest.php @@ -0,0 +1,437 @@ +assertEquals('123', $command->get('test')); + $this->assertFalse($command->isPrepared()); + $this->assertFalse($command->isExecuted()); + } + + public function testDeterminesShortName() + { + $api = new Operation(array('name' => 'foobar')); + $command = new MockCommand(array(), $api); + $this->assertEquals('foobar', $command->getName()); + + $command = new MockCommand(); + $this->assertEquals('mock_command', $command->getName()); + + $command = new Sub(); + $this->assertEquals('sub.sub', $command->getName()); + } + + /** + * @expectedException RuntimeException + */ + public function testGetRequestThrowsExceptionBeforePreparation() + { + $command = new MockCommand(); + $command->getRequest(); + } + + public function testGetResponseExecutesCommandsWhenNeeded() + { + $response = new Response(200); + $client = $this->getClient(); + $this->setMockResponse($client, array($response)); + $command = new MockCommand(); + $command->setClient($client); + $this->assertSame($response, $command->getResponse()); + $this->assertSame($response, $command->getResponse()); + } + + public function testGetResultExecutesCommandsWhenNeeded() + { + $response = new Response(200); + $client = $this->getClient(); + $this->setMockResponse($client, array($response)); + $command = new MockCommand(); + $command->setClient($client); + $this->assertSame($response, $command->getResult()); + $this->assertSame($response, $command->getResult()); + } + + public function testSetClient() + { + $command = new MockCommand(); + $client = $this->getClient(); + + $command->setClient($client); + $this->assertEquals($client, $command->getClient()); + + unset($client); + unset($command); + + $command = new MockCommand(); + $client = $this->getClient(); + + $command->setClient($client)->prepare(); + $this->assertEquals($client, $command->getClient()); + $this->assertTrue($command->isPrepared()); + } + + public function testExecute() + { + $client = $this->getClient(); + $response = new Response(200, array( + 'Content-Type' => 'application/xml' + ), '123'); + $this->setMockResponse($client, array($response)); + $command = new MockCommand(); + $this->assertSame($command, $command->setClient($client)); + + // Returns the result of the command + $this->assertInstanceOf('SimpleXMLElement', $command->execute()); + + $this->assertTrue($command->isPrepared()); + $this->assertTrue($command->isExecuted()); + $this->assertSame($response, $command->getResponse()); + $this->assertInstanceOf('Guzzle\\Http\\Message\\Request', $command->getRequest()); + // Make sure that the result was automatically set to a SimpleXMLElement + $this->assertInstanceOf('SimpleXMLElement', $command->getResult()); + $this->assertEquals('123', (string) $command->getResult()->data); + } + + public function testConvertsJsonResponsesToArray() + { + $client = $this->getClient(); + $this->setMockResponse($client, array( + new \Guzzle\Http\Message\Response(200, array( + 'Content-Type' => 'application/json' + ), '{ "key": "Hi!" }' + ) + )); + $command = new MockCommand(); + $command->setClient($client); + $command->execute(); + $this->assertEquals(array( + 'key' => 'Hi!' + ), $command->getResult()); + } + + /** + * @expectedException \Guzzle\Common\Exception\RuntimeException + */ + public function testConvertsInvalidJsonResponsesToArray() + { + $client = $this->getClient(); + $this->setMockResponse($client, array( + new \Guzzle\Http\Message\Response(200, array( + 'Content-Type' => 'application/json' + ), '{ "key": "Hi!" }invalid' + ) + )); + $command = new MockCommand(); + $command->setClient($client); + $command->execute(); + } + + public function testProcessResponseIsNotXml() + { + $client = $this->getClient(); + $this->setMockResponse($client, array( + new Response(200, array( + 'Content-Type' => 'application/octet-stream' + ), 'abc,def,ghi') + )); + $command = new MockCommand(); + $client->execute($command); + + // Make sure that the result was not converted to XML + $this->assertFalse($command->getResult() instanceof \SimpleXMLElement); + } + + /** + * @expectedException RuntimeException + */ + public function testExecuteThrowsExceptionWhenNoClientIsSet() + { + $command = new MockCommand(); + $command->execute(); + } + + /** + * @expectedException RuntimeException + */ + public function testPrepareThrowsExceptionWhenNoClientIsSet() + { + $command = new MockCommand(); + $command->prepare(); + } + + public function testCommandsAllowsCustomRequestHeaders() + { + $command = new MockCommand(); + $command->getRequestHeaders()->set('test', '123'); + $this->assertInstanceOf('Guzzle\Common\Collection', $command->getRequestHeaders()); + $this->assertEquals('123', $command->getRequestHeaders()->get('test')); + + $command->setClient($this->getClient())->prepare(); + $this->assertEquals('123', (string) $command->getRequest()->getHeader('test')); + } + + public function testCommandsAllowsCustomRequestHeadersAsArray() + { + $command = new MockCommand(array(AbstractCommand::HEADERS_OPTION => array('Foo' => 'Bar'))); + $this->assertInstanceOf('Guzzle\Common\Collection', $command->getRequestHeaders()); + $this->assertEquals('Bar', $command->getRequestHeaders()->get('Foo')); + } + + private function getOperation() + { + return new Operation(array( + 'name' => 'foobar', + 'httpMethod' => 'POST', + 'class' => 'Guzzle\\Tests\\Service\\Mock\\Command\\MockCommand', + 'parameters' => array( + 'test' => array( + 'default' => '123', + 'type' => 'string' + ) + ))); + } + + public function testCommandsUsesOperation() + { + $api = $this->getOperation(); + $command = new MockCommand(array(), $api); + $this->assertSame($api, $command->getOperation()); + $command->setClient($this->getClient())->prepare(); + $this->assertEquals('123', $command->get('test')); + $this->assertSame($api, $command->getOperation($api)); + } + + public function testCloneMakesNewRequest() + { + $client = $this->getClient(); + $command = new MockCommand(array(), $this->getOperation()); + $command->setClient($client); + + $command->prepare(); + $this->assertTrue($command->isPrepared()); + + $command2 = clone $command; + $this->assertFalse($command2->isPrepared()); + } + + public function testHasOnCompleteMethod() + { + $that = $this; + $called = 0; + + $testFunction = function($command) use (&$called, $that) { + $called++; + $that->assertInstanceOf('Guzzle\Service\Command\CommandInterface', $command); + }; + + $client = $this->getClient(); + $command = new MockCommand(array( + 'command.on_complete' => $testFunction + ), $this->getOperation()); + $command->setClient($client); + + $command->prepare()->setResponse(new Response(200), true); + $command->execute(); + $this->assertEquals(1, $called); + } + + /** + * @expectedException \Guzzle\Common\Exception\InvalidArgumentException + */ + public function testOnCompleteMustBeCallable() + { + $client = $this->getClient(); + $command = new MockCommand(); + $command->setOnComplete('foo'); + } + + public function testCanSetResultManually() + { + $client = $this->getClient(); + $client->getEventDispatcher()->addSubscriber(new MockPlugin(array( + new Response(200) + ))); + $command = new MockCommand(); + $client->execute($command); + $command->setResult('foo!'); + $this->assertEquals('foo!', $command->getResult()); + } + + public function testCanInitConfig() + { + $command = $this->getMockBuilder('Guzzle\\Service\\Command\\AbstractCommand') + ->setConstructorArgs(array(array( + 'foo' => 'bar' + ), new Operation(array( + 'parameters' => array( + 'baz' => new Parameter(array( + 'default' => 'baaar' + )) + ) + )))) + ->getMockForAbstractClass(); + + $this->assertEquals('bar', $command['foo']); + $this->assertEquals('baaar', $command['baz']); + } + + public function testAddsCurlOptionsToRequestsWhenPreparing() + { + $command = new MockCommand(array( + 'foo' => 'bar', + 'curl.options' => array('CURLOPT_PROXYPORT' => 8080) + )); + $client = new Client(); + $command->setClient($client); + $request = $command->prepare(); + $this->assertEquals(8080, $request->getCurlOptions()->get(CURLOPT_PROXYPORT)); + } + + public function testIsInvokable() + { + $client = $this->getClient(); + $response = new Response(200); + $this->setMockResponse($client, array($response)); + $command = new MockCommand(); + $command->setClient($client); + // Returns the result of the command + $this->assertSame($response, $command()); + } + + public function testCreatesDefaultOperation() + { + $command = $this->getMockBuilder('Guzzle\Service\Command\AbstractCommand')->getMockForAbstractClass(); + $this->assertInstanceOf('Guzzle\Service\Description\Operation', $command->getOperation()); + } + + public function testAllowsValidatorToBeInjected() + { + $command = $this->getMockBuilder('Guzzle\Service\Command\AbstractCommand')->getMockForAbstractClass(); + $v = new SchemaValidator(); + $command->setValidator($v); + $this->assertSame($v, $this->readAttribute($command, 'validator')); + } + + public function testCanDisableValidation() + { + $command = new MockCommand(); + $command->setClient(new \Guzzle\Service\Client()); + $v = $this->getMockBuilder('Guzzle\Service\Description\SchemaValidator') + ->setMethods(array('validate')) + ->getMock(); + $v->expects($this->never())->method('validate'); + $command->setValidator($v); + $command->set(AbstractCommand::DISABLE_VALIDATION, true); + $command->prepare(); + } + + public function testValidatorDoesNotUpdateNonDefaultValues() + { + $command = new MockCommand(array('test' => 123, 'foo' => 'bar')); + $command->setClient(new \Guzzle\Service\Client()); + $command->prepare(); + $this->assertEquals(123, $command->get('test')); + $this->assertEquals('bar', $command->get('foo')); + } + + public function testValidatorUpdatesDefaultValues() + { + $command = new MockCommand(); + $command->setClient(new \Guzzle\Service\Client()); + $command->prepare(); + $this->assertEquals(123, $command->get('test')); + $this->assertEquals('abc', $command->get('_internal')); + } + + /** + * @expectedException \Guzzle\Service\Exception\ValidationException + * @expectedExceptionMessage [Foo] Baz + */ + public function testValidatesCommandBeforeSending() + { + $command = new MockCommand(); + $command->setClient(new \Guzzle\Service\Client()); + $v = $this->getMockBuilder('Guzzle\Service\Description\SchemaValidator') + ->setMethods(array('validate', 'getErrors')) + ->getMock(); + $v->expects($this->any())->method('validate')->will($this->returnValue(false)); + $v->expects($this->any())->method('getErrors')->will($this->returnValue(array('[Foo] Baz', '[Bar] Boo'))); + $command->setValidator($v); + $command->prepare(); + } + + /** + * @expectedException \Guzzle\Service\Exception\ValidationException + * @expectedExceptionMessage Validation errors: [abc] must be of type string + */ + public function testValidatesAdditionalParameters() + { + $description = ServiceDescription::factory(array( + 'operations' => array( + 'foo' => array( + 'parameters' => array( + 'baz' => array('type' => 'integer') + ), + 'additionalParameters' => array( + 'type' => 'string' + ) + ) + ) + )); + + $client = new Client(); + $client->setDescription($description); + $command = $client->getCommand('foo', array( + 'abc' => false, + 'command.headers' => array('foo' => 'bar') + )); + $command->prepare(); + } + + public function testCanAccessValidationErrorsFromCommand() + { + $validationErrors = array('[Foo] Baz', '[Bar] Boo'); + $command = new MockCommand(); + $command->setClient(new \Guzzle\Service\Client()); + + $this->assertFalse($command->getValidationErrors()); + + $v = $this->getMockBuilder('Guzzle\Service\Description\SchemaValidator') + ->setMethods(array('validate', 'getErrors')) + ->getMock(); + $v->expects($this->any())->method('getErrors')->will($this->returnValue($validationErrors)); + $command->setValidator($v); + + $this->assertEquals($validationErrors, $command->getValidationErrors()); + } + + public function testCanChangeResponseBody() + { + $body = EntityBody::factory(); + $command = new MockCommand(); + $command->setClient(new \Guzzle\Service\Client()); + $command->set(AbstractCommand::RESPONSE_BODY, $body); + $request = $command->prepare(); + $this->assertSame($body, $this->readAttribute($request, 'responseBody')); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/DefaultRequestSerializerTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/DefaultRequestSerializerTest.php new file mode 100644 index 000000000..b7a4682fc --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/DefaultRequestSerializerTest.php @@ -0,0 +1,122 @@ +serializer = DefaultRequestSerializer::getInstance(); + $this->client = new Client('http://foo.com/baz'); + $this->operation = new Operation(array('httpMethod' => 'POST')); + $this->command = $this->getMockBuilder('Guzzle\Service\Command\AbstractCommand') + ->setConstructorArgs(array(array(), $this->operation)) + ->getMockForAbstractClass(); + $this->command->setClient($this->client); + } + + public function testAllowsCustomVisitor() + { + $this->serializer->addVisitor('custom', new HeaderVisitor()); + $this->command['test'] = '123'; + $this->operation->addParam(new Parameter(array('name' => 'test', 'location' => 'custom'))); + $request = $this->serializer->prepare($this->command); + $this->assertEquals('123', (string) $request->getHeader('test')); + } + + public function testUsesRelativePath() + { + $this->operation->setUri('bar'); + $request = $this->serializer->prepare($this->command); + $this->assertEquals('http://foo.com/baz/bar', (string) $request->getUrl()); + } + + public function testUsesRelativePathWithUriLocations() + { + $this->command['test'] = '123'; + $this->operation->setUri('bar/{test}'); + $this->operation->addParam(new Parameter(array('name' => 'test', 'location' => 'uri'))); + $request = $this->serializer->prepare($this->command); + $this->assertEquals('http://foo.com/baz/bar/123', (string) $request->getUrl()); + } + + public function testAllowsCustomFactory() + { + $f = new VisitorFlyweight(); + $serializer = new DefaultRequestSerializer($f); + $this->assertSame($f, $this->readAttribute($serializer, 'factory')); + } + + public function testMixedParams() + { + $this->operation->setUri('bar{?limit,fields}'); + $this->operation->addParam(new Parameter(array( + 'name' => 'limit', + 'location' => 'uri', + 'required' => false, + ))); + $this->operation->addParam(new Parameter(array( + 'name' => 'fields', + 'location' => 'uri', + 'required' => true, + ))); + + $this->command['fields'] = array('id', 'name'); + + $request = $this->serializer->prepare($this->command); + $this->assertEquals('http://foo.com/baz/bar?fields='.urlencode('id,name'), (string) $request->getUrl()); + } + + public function testValidatesAdditionalParameters() + { + $description = ServiceDescription::factory(array( + 'operations' => array( + 'foo' => array( + 'httpMethod' => 'PUT', + 'parameters' => array( + 'bar' => array('location' => 'header') + ), + 'additionalParameters' => array( + 'location' => 'json' + ) + ) + ) + )); + + $client = new Client(); + $client->setDescription($description); + $command = $client->getCommand('foo'); + $command['bar'] = 'test'; + $command['hello'] = 'abc'; + $request = $command->prepare(); + $this->assertEquals('test', (string) $request->getHeader('bar')); + $this->assertEquals('{"hello":"abc"}', (string) $request->getBody()); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/DefaultResponseParserTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/DefaultResponseParserTest.php new file mode 100644 index 000000000..a6a02f951 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/DefaultResponseParserTest.php @@ -0,0 +1,59 @@ +setClient(new Client()); + $request = $op->prepare(); + $request->setResponse(new Response(200, array( + 'Content-Type' => 'application/xml' + ), 'Bar'), true); + $this->assertInstanceOf('SimpleXMLElement', $op->execute()); + } + + public function testParsesJsonResponses() + { + $op = new OperationCommand(array(), new Operation()); + $op->setClient(new Client()); + $request = $op->prepare(); + $request->setResponse(new Response(200, array( + 'Content-Type' => 'application/json' + ), '{"Baz":"Bar"}'), true); + $this->assertEquals(array('Baz' => 'Bar'), $op->execute()); + } + + /** + * @expectedException \Guzzle\Common\Exception\RuntimeException + */ + public function testThrowsExceptionWhenParsingJsonFails() + { + $op = new OperationCommand(array(), new Operation()); + $op->setClient(new Client()); + $request = $op->prepare(); + $request->setResponse(new Response(200, array('Content-Type' => 'application/json'), '{"Baz":ddw}'), true); + $op->execute(); + } + + public function testAddsContentTypeWhenExpectsIsSetOnCommand() + { + $op = new OperationCommand(array(), new Operation()); + $op['command.expects'] = 'application/json'; + $op->setClient(new Client()); + $request = $op->prepare(); + $request->setResponse(new Response(200, null, '{"Baz":"Bar"}'), true); + $this->assertEquals(array('Baz' => 'Bar'), $op->execute()); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/Factory/AliasFactoryTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/Factory/AliasFactoryTest.php new file mode 100644 index 000000000..ab1041ad3 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/Factory/AliasFactoryTest.php @@ -0,0 +1,76 @@ +client = new Client(); + + $map = new MapFactory(array( + 'test' => 'Guzzle\Tests\Service\Mock\Command\MockCommand', + 'test1' => 'Guzzle\Tests\Service\Mock\Command\OtherCommand' + )); + + $this->factory = new AliasFactory($this->client, array( + 'foo' => 'test', + 'bar' => 'sub', + 'sub' => 'test1', + 'krull' => 'test3', + 'krull_2' => 'krull', + 'sub_2' => 'bar', + 'bad_link' => 'jarjar' + )); + + $map2 = new MapFactory(array( + 'test3' => 'Guzzle\Tests\Service\Mock\Command\Sub\Sub' + )); + + $this->client->setCommandFactory(new CompositeFactory(array($map, $this->factory, $map2))); + } + + public function aliasProvider() + { + return array( + array('foo', 'Guzzle\Tests\Service\Mock\Command\MockCommand', false), + array('bar', 'Guzzle\Tests\Service\Mock\Command\OtherCommand', false), + array('sub', 'Guzzle\Tests\Service\Mock\Command\OtherCommand', false), + array('sub_2', 'Guzzle\Tests\Service\Mock\Command\OtherCommand', false), + array('krull', 'Guzzle\Tests\Service\Mock\Command\Sub\Sub', false), + array('krull_2', 'Guzzle\Tests\Service\Mock\Command\Sub\Sub', false), + array('missing', null, true), + array('bad_link', null, true) + ); + } + + /** + * @dataProvider aliasProvider + */ + public function testAliasesCommands($key, $result, $exception) + { + try { + $command = $this->client->getCommand($key); + if (is_null($result)) { + $this->assertNull($command); + } else { + $this->assertInstanceof($result, $command); + } + } catch (\Exception $e) { + if (!$exception) { + $this->fail('Got exception when it was not expected'); + } + } + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/Factory/CompositeFactoryTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/Factory/CompositeFactoryTest.php new file mode 100644 index 000000000..b896dcfd6 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/Factory/CompositeFactoryTest.php @@ -0,0 +1,124 @@ +getMockBuilder($class) + ->disableOriginalConstructor() + ->getMock(); + } + + public function testIsIterable() + { + $factory = new CompositeFactory(array($this->getFactory(), $this->getFactory())); + $this->assertEquals(2, count($factory)); + $this->assertEquals(2, count(iterator_to_array($factory->getIterator()))); + } + + public function testFindsFactories() + { + $f1 = $this->getFactory(); + $f2 = $this->getFactory('Guzzle\\Service\\Command\\Factory\\CompositeFactory'); + $factory = new CompositeFactory(array($f1, $f2)); + $this->assertNull($factory->find('foo')); + $this->assertNull($factory->find($this->getFactory())); + $this->assertSame($f1, $factory->find('Guzzle\\Service\\Command\\Factory\\MapFactory')); + $this->assertSame($f2, $factory->find('Guzzle\\Service\\Command\\Factory\\CompositeFactory')); + $this->assertSame($f1, $factory->find($f1)); + $this->assertSame($f2, $factory->find($f2)); + + $this->assertFalse($factory->has('foo')); + $this->assertTrue($factory->has('Guzzle\\Service\\Command\\Factory\\MapFactory')); + $this->assertTrue($factory->has('Guzzle\\Service\\Command\\Factory\\CompositeFactory')); + } + + public function testCreatesCommands() + { + $factory = new CompositeFactory(); + $this->assertNull($factory->factory('foo')); + + $f1 = $this->getFactory(); + $mockCommand1 = $this->getMockForAbstractClass('Guzzle\\Service\\Command\\AbstractCommand'); + + $f1->expects($this->once()) + ->method('factory') + ->with($this->equalTo('foo')) + ->will($this->returnValue($mockCommand1)); + + $factory = new CompositeFactory(array($f1)); + $this->assertSame($mockCommand1, $factory->factory('foo')); + } + + public function testAllowsRemovalOfFactories() + { + $f1 = $this->getFactory(); + $f2 = $this->getFactory(); + $f3 = $this->getFactory('Guzzle\\Service\\Command\\Factory\\CompositeFactory'); + $factories = array($f1, $f2, $f3); + $factory = new CompositeFactory($factories); + + $factory->remove('foo'); + $this->assertEquals($factories, $factory->getIterator()->getArrayCopy()); + + $factory->remove($f1); + $this->assertEquals(array($f2, $f3), $factory->getIterator()->getArrayCopy()); + + $factory->remove('Guzzle\\Service\\Command\\Factory\\MapFactory'); + $this->assertEquals(array($f3), $factory->getIterator()->getArrayCopy()); + + $factory->remove('Guzzle\\Service\\Command\\Factory\\CompositeFactory'); + $this->assertEquals(array(), $factory->getIterator()->getArrayCopy()); + + $factory->remove('foo'); + $this->assertEquals(array(), $factory->getIterator()->getArrayCopy()); + } + + public function testAddsFactoriesBeforeAndAtEnd() + { + $f1 = $this->getFactory(); + $f2 = $this->getFactory(); + $f3 = $this->getFactory('Guzzle\\Service\\Command\\Factory\\CompositeFactory'); + $f4 = $this->getFactory(); + + $factory = new CompositeFactory(); + + $factory->add($f1); + $this->assertEquals(array($f1), $factory->getIterator()->getArrayCopy()); + + $factory->add($f2); + $this->assertEquals(array($f1, $f2), $factory->getIterator()->getArrayCopy()); + + $factory->add($f3, $f2); + $this->assertEquals(array($f1, $f3, $f2), $factory->getIterator()->getArrayCopy()); + + $factory->add($f4, 'Guzzle\\Service\\Command\\Factory\\CompositeFactory'); + $this->assertEquals(array($f1, $f4, $f3, $f2), $factory->getIterator()->getArrayCopy()); + } + + public function testProvidesDefaultChainForClients() + { + $client = $this->getMock('Guzzle\\Service\\Client'); + $chain = CompositeFactory::getDefaultChain($client); + $a = $chain->getIterator()->getArrayCopy(); + $this->assertEquals(1, count($a)); + $this->assertInstanceOf('Guzzle\\Service\\Command\\Factory\\ConcreteClassFactory', $a[0]); + + $description = $this->getMock('Guzzle\\Service\\Description\\ServiceDescription'); + $client->expects($this->once()) + ->method('getDescription') + ->will($this->returnValue($description)); + $chain = CompositeFactory::getDefaultChain($client); + $a = $chain->getIterator()->getArrayCopy(); + $this->assertEquals(2, count($a)); + $this->assertInstanceOf('Guzzle\\Service\\Command\\Factory\\ServiceDescriptionFactory', $a[0]); + $this->assertInstanceOf('Guzzle\\Service\\Command\\Factory\\ConcreteClassFactory', $a[1]); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/Factory/ConcreteClassFactoryTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/Factory/ConcreteClassFactoryTest.php new file mode 100644 index 000000000..766471822 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/Factory/ConcreteClassFactoryTest.php @@ -0,0 +1,49 @@ + $prefix + )); + } + + $factory = new ConcreteClassFactory($client); + + if (is_null($result)) { + $this->assertNull($factory->factory($key)); + } else { + $this->assertInstanceof($result, $factory->factory($key)); + } + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/Factory/MapFactoryTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/Factory/MapFactoryTest.php new file mode 100644 index 000000000..ee720d1e7 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/Factory/MapFactoryTest.php @@ -0,0 +1,37 @@ + 'Guzzle\Tests\Service\Mock\Command\MockCommand', + 'test1' => 'Guzzle\Tests\Service\Mock\Command\OtherCommand' + )); + + if (is_null($result)) { + $this->assertNull($factory->factory($key)); + } else { + $this->assertInstanceof($result, $factory->factory($key)); + } + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/Factory/ServiceDescriptionFactoryTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/Factory/ServiceDescriptionFactoryTest.php new file mode 100644 index 000000000..337263481 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/Factory/ServiceDescriptionFactoryTest.php @@ -0,0 +1,68 @@ +getDescription(); + + $factory = new ServiceDescriptionFactory($d); + $this->assertSame($d, $factory->getServiceDescription()); + + if (is_null($result)) { + $this->assertNull($factory->factory($key)); + } else { + $this->assertInstanceof($result, $factory->factory($key)); + } + } + + public function testUsesUcFirstIfNoExactMatch() + { + $d = $this->getDescription(); + $factory = new ServiceDescriptionFactory($d, new Inflector()); + $this->assertInstanceof('Guzzle\Tests\Service\Mock\Command\OtherCommand', $factory->factory('Test')); + $this->assertInstanceof('Guzzle\Tests\Service\Mock\Command\OtherCommand', $factory->factory('test')); + } + + public function testUsesInflectionIfNoExactMatch() + { + $d = $this->getDescription(); + $factory = new ServiceDescriptionFactory($d, new Inflector()); + $this->assertInstanceof('Guzzle\Tests\Service\Mock\Command\OtherCommand', $factory->factory('Binks')); + $this->assertInstanceof('Guzzle\Tests\Service\Mock\Command\OtherCommand', $factory->factory('binks')); + $this->assertInstanceof('Guzzle\Tests\Service\Mock\Command\MockCommand', $factory->factory('JarJar')); + $this->assertInstanceof('Guzzle\Tests\Service\Mock\Command\MockCommand', $factory->factory('jar_jar')); + } + + protected function getDescription() + { + return ServiceDescription::factory(array( + 'operations' => array( + 'jar_jar' => array('class' => 'Guzzle\Tests\Service\Mock\Command\MockCommand'), + 'binks' => array('class' => 'Guzzle\Tests\Service\Mock\Command\OtherCommand'), + 'Test' => array('class' => 'Guzzle\Tests\Service\Mock\Command\OtherCommand') + ) + )); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Request/AbstractVisitorTestCase.php b/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Request/AbstractVisitorTestCase.php new file mode 100644 index 000000000..46b472eb5 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Request/AbstractVisitorTestCase.php @@ -0,0 +1,110 @@ +command = new MockCommand(); + $this->request = new EntityEnclosingRequest('POST', 'http://www.test.com/some/path.php'); + $this->validator = new SchemaValidator(); + } + + protected function getCommand($location) + { + $command = new OperationCommand(array(), $this->getNestedCommand($location)); + $command->setClient(new MockClient()); + + return $command; + } + + protected function getNestedCommand($location) + { + return new Operation(array( + 'httpMethod' => 'POST', + 'parameters' => array( + 'foo' => new Parameter(array( + 'type' => 'object', + 'location' => $location, + 'sentAs' => 'Foo', + 'required' => true, + 'properties' => array( + 'test' => array( + 'type' => 'object', + 'required' => true, + 'properties' => array( + 'baz' => array( + 'type' => 'boolean', + 'default' => true + ), + 'jenga' => array( + 'type' => 'string', + 'default' => 'hello', + 'sentAs' => 'Jenga_Yall!', + 'filters' => array('strtoupper') + ) + ) + ), + 'bar' => array('default' => 123) + ), + 'additionalProperties' => array( + 'type' => 'string', + 'filters' => array('strtoupper'), + 'location' => $location + ) + )), + 'arr' => new Parameter(array( + 'type' => 'array', + 'location' => $location, + 'items' => array( + 'type' => 'string', + 'filters' => array('strtoupper') + ) + )), + ) + )); + } + + protected function getCommandWithArrayParamAndFilters() + { + $operation = new Operation(array( + 'httpMethod' => 'POST', + 'parameters' => array( + 'foo' => new Parameter(array( + 'type' => 'string', + 'location' => 'query', + 'sentAs' => 'Foo', + 'required' => true, + 'default' => 'bar', + 'filters' => array('strtoupper') + )), + 'arr' => new Parameter(array( + 'type' => 'array', + 'location' => 'query', + 'sentAs' => 'Arr', + 'required' => true, + 'default' => array(123, 456, 789), + 'filters' => array(array('method' => 'implode', 'args' => array(',', '@value'))) + )) + ) + )); + $command = new OperationCommand(array(), $operation); + $command->setClient(new MockClient()); + + return $command; + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Request/BodyVisitorTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Request/BodyVisitorTest.php new file mode 100644 index 000000000..2a95c452a --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Request/BodyVisitorTest.php @@ -0,0 +1,63 @@ +getNestedCommand('body')->getParam('foo')->setSentAs('Foo'); + $visitor->visit($this->command, $this->request, $param, '123'); + $this->assertEquals('123', (string) $this->request->getBody()); + $this->assertNull($this->request->getHeader('Expect')); + } + + public function testAddsExpectHeaderWhenSetToTrue() + { + $visitor = new Visitor(); + $param = $this->getNestedCommand('body')->getParam('foo')->setSentAs('Foo'); + $param->setData('expect_header', true); + $visitor->visit($this->command, $this->request, $param, '123'); + $this->assertEquals('123', (string) $this->request->getBody()); + } + + public function testCanDisableExpectHeader() + { + $visitor = new Visitor(); + $param = $this->getNestedCommand('body')->getParam('foo')->setSentAs('Foo'); + $param->setData('expect_header', false); + $visitor->visit($this->command, $this->request, $param, '123'); + $this->assertNull($this->request->getHeader('Expect')); + } + + public function testCanSetExpectHeaderBasedOnSize() + { + $visitor = new Visitor(); + $param = $this->getNestedCommand('body')->getParam('foo')->setSentAs('Foo'); + // The body is less than the cutoff + $param->setData('expect_header', 5); + $visitor->visit($this->command, $this->request, $param, '123'); + $this->assertNull($this->request->getHeader('Expect')); + // Now check when the body is greater than the cutoff + $param->setData('expect_header', 2); + $visitor->visit($this->command, $this->request, $param, '123'); + $this->assertEquals('100-Continue', (string) $this->request->getHeader('Expect')); + } + + public function testAddsContentEncodingWhenSetOnBody() + { + $visitor = new Visitor(); + $param = $this->getNestedCommand('body')->getParam('foo')->setSentAs('Foo'); + $body = EntityBody::factory('foo'); + $body->compress(); + $visitor->visit($this->command, $this->request, $param, $body); + $this->assertEquals('gzip', (string) $this->request->getHeader('Content-Encoding')); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Request/HeaderVisitorTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Request/HeaderVisitorTest.php new file mode 100644 index 000000000..7ea1ae913 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Request/HeaderVisitorTest.php @@ -0,0 +1,48 @@ +getNestedCommand('header')->getParam('foo')->setSentAs('test'); + $param->setAdditionalProperties(new Parameter(array())); + $visitor->visit($this->command, $this->request, $param, 'test'); + } + + public function testVisitsLocation() + { + $visitor = new Visitor(); + $param = $this->getNestedCommand('header')->getParam('foo')->setSentAs('test'); + $param->setAdditionalProperties(false); + $visitor->visit($this->command, $this->request, $param, '123'); + $this->assertEquals('123', (string) $this->request->getHeader('test')); + } + + public function testVisitsMappedPrefixHeaders() + { + $visitor = new Visitor(); + $param = $this->getNestedCommand('header')->getParam('foo')->setSentAs('test'); + $param->setSentAs('x-foo-'); + $param->setAdditionalProperties(new Parameter(array( + 'type' => 'string' + ))); + $visitor->visit($this->command, $this->request, $param, array( + 'bar' => 'test', + 'baz' => '123' + )); + $this->assertEquals('test', (string) $this->request->getHeader('x-foo-bar')); + $this->assertEquals('123', (string) $this->request->getHeader('x-foo-baz')); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Request/JsonVisitorTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Request/JsonVisitorTest.php new file mode 100644 index 000000000..ea6782f53 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Request/JsonVisitorTest.php @@ -0,0 +1,60 @@ +after($this->command, $this->request); + + $param = $this->getNestedCommand('json')->getParam('foo'); + $visitor->visit($this->command, $this->request, $param->setSentAs('test'), '123'); + $visitor->visit($this->command, $this->request, $param->setSentAs('test2'), 'abc'); + $visitor->after($this->command, $this->request); + $this->assertEquals('{"test":"123","test2":"abc"}', (string) $this->request->getBody()); + } + + public function testAddsJsonHeader() + { + $visitor = new Visitor(); + $visitor->setContentTypeHeader('application/json-foo'); + $param = $this->getNestedCommand('json')->getParam('foo'); + $visitor->visit($this->command, $this->request, $param->setSentAs('test'), '123'); + $visitor->after($this->command, $this->request); + $this->assertEquals('application/json-foo', (string) $this->request->getHeader('Content-Type')); + } + + public function testRecursivelyBuildsJsonBodies() + { + $command = $this->getCommand('json'); + $request = $command->prepare(); + $this->assertEquals('{"Foo":{"test":{"baz":true,"Jenga_Yall!":"HELLO"},"bar":123}}', (string) $request->getBody()); + } + + public function testAppliesFiltersToAdditionalProperties() + { + $command = $this->getCommand('json'); + $command->set('foo', array('not_set' => 'abc')); + $request = $command->prepare(); + $result = json_decode($request->getBody(), true); + $this->assertEquals('ABC', $result['Foo']['not_set']); + } + + public function testAppliesFiltersToArrayItemValues() + { + $command = $this->getCommand('json'); + $command->set('arr', array('a', 'b')); + $request = $command->prepare(); + $result = json_decode($request->getBody(), true); + $this->assertEquals(array('A', 'B'), $result['arr']); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Request/PostFieldVisitorTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Request/PostFieldVisitorTest.php new file mode 100644 index 000000000..540b41087 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Request/PostFieldVisitorTest.php @@ -0,0 +1,33 @@ +getNestedCommand('postField')->getParam('foo'); + $visitor->visit($this->command, $this->request, $param->setSentAs('test'), '123'); + $this->assertEquals('123', (string) $this->request->getPostField('test')); + } + + public function testRecursivelyBuildsPostFields() + { + $command = $this->getCommand('postField'); + $request = $command->prepare(); + $visitor = new Visitor(); + $param = $command->getOperation()->getParam('foo'); + $visitor->visit($command, $request, $param, $command['foo']); + $visitor->after($command, $request); + $this->assertEquals( + 'Foo[test][baz]=1&Foo[test][Jenga_Yall!]=HELLO&Foo[bar]=123', + rawurldecode((string) $request->getPostFields()) + ); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Request/PostFileVisitorTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Request/PostFileVisitorTest.php new file mode 100644 index 000000000..21e3cec33 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Request/PostFileVisitorTest.php @@ -0,0 +1,54 @@ +getNestedCommand('postFile')->getParam('foo'); + + // Test using a path to a file + $visitor->visit($this->command, $this->request, $param->setSentAs('test_3'), __FILE__); + $this->assertInternalType('array', $this->request->getPostFile('test_3')); + + // Test with a PostFile + $visitor->visit($this->command, $this->request, $param->setSentAs(null), new PostFile('baz', __FILE__)); + $this->assertInternalType('array', $this->request->getPostFile('baz')); + } + + public function testVisitsLocationWithMultipleFiles() + { + $description = ServiceDescription::factory(array( + 'operations' => array( + 'DoPost' => array( + 'httpMethod' => 'POST', + 'parameters' => array( + 'foo' => array( + 'location' => 'postFile', + 'type' => array('string', 'array') + ) + ) + ) + ) + )); + $this->getServer()->flush(); + $this->getServer()->enqueue(array("HTTP/1.1 200 OK\r\nContent-Length:0\r\n\r\n")); + $client = new Client($this->getServer()->getUrl()); + $client->setDescription($description); + $command = $client->getCommand('DoPost', array('foo' => array(__FILE__, __FILE__))); + $command->execute(); + $received = $this->getServer()->getReceivedRequests(); + $this->assertContains('name="foo[0]";', $received[0]); + $this->assertContains('name="foo[1]";', $received[0]); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Request/QueryVisitorTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Request/QueryVisitorTest.php new file mode 100644 index 000000000..607af7603 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Request/QueryVisitorTest.php @@ -0,0 +1,48 @@ +getNestedCommand('query')->getParam('foo')->setSentAs('test'); + $visitor->visit($this->command, $this->request, $param, '123'); + $this->assertEquals('123', $this->request->getQuery()->get('test')); + } + + /** + * @covers Guzzle\Service\Command\LocationVisitor\Request\QueryVisitor + * @covers Guzzle\Service\Command\LocationVisitor\Request\AbstractRequestVisitor::resolveRecursively + */ + public function testRecursivelyBuildsQueryStrings() + { + $command = $this->getCommand('query'); + $command->getOperation()->getParam('foo')->setSentAs('Foo'); + $request = $command->prepare(); + $this->assertEquals( + 'Foo[test][baz]=1&Foo[test][Jenga_Yall!]=HELLO&Foo[bar]=123', + rawurldecode($request->getQuery()) + ); + } + + /** + * @covers Guzzle\Service\Command\LocationVisitor\Request\AbstractRequestVisitor::resolveRecursively + */ + public function testFiltersAreAppliedToArrayParamType() + { + $command = $this->getCommandWithArrayParamAndFilters(); + $request = $command->prepare(); + $query = $request->getQuery(); + // param type 'string' + $this->assertEquals('BAR', $query->get('Foo')); + // param type 'array' + $this->assertEquals('123,456,789', $query->get('Arr')); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Request/ResponseBodyVisitorTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Request/ResponseBodyVisitorTest.php new file mode 100644 index 000000000..ff8cec599 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Request/ResponseBodyVisitorTest.php @@ -0,0 +1,20 @@ +getNestedCommand('response_body')->getParam('foo'); + $visitor->visit($this->command, $this->request, $param, sys_get_temp_dir() . '/foo.txt'); + $body = $this->readAttribute($this->request, 'responseBody'); + $this->assertContains('/foo.txt', $body->getUri()); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Request/XmlVisitorTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Request/XmlVisitorTest.php new file mode 100644 index 000000000..beb58b00a --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Request/XmlVisitorTest.php @@ -0,0 +1,558 @@ + array( + 'xmlRoot' => array( + 'name' => 'test', + 'namespaces' => 'http://foo.com' + ) + ), + 'parameters' => array( + 'Foo' => array('location' => 'xml', 'type' => 'string'), + 'Baz' => array('location' => 'xml', 'type' => 'string') + ) + ), + array('Foo' => 'test', 'Baz' => 'bar'), + 'testbar' + ), + // Ensure that the content-type is not added + array(array('parameters' => array('Foo' => array('location' => 'xml', 'type' => 'string'))), array(), ''), + // Test with adding attributes and no namespace + array( + array( + 'data' => array( + 'xmlRoot' => array( + 'name' => 'test' + ) + ), + 'parameters' => array( + 'Foo' => array('location' => 'xml', 'type' => 'string', 'data' => array('xmlAttribute' => true)) + ) + ), + array('Foo' => 'test', 'Baz' => 'bar'), + '' + ), + // Test adding with an array + array( + array( + 'parameters' => array( + 'Foo' => array('location' => 'xml', 'type' => 'string'), + 'Baz' => array( + 'type' => 'array', + 'location' => 'xml', + 'items' => array( + 'type' => 'numeric', + 'sentAs' => 'Bar' + ) + ) + ) + ), + array('Foo' => 'test', 'Baz' => array(1, 2)), + 'test12' + ), + // Test adding an object + array( + array( + 'parameters' => array( + 'Foo' => array('location' => 'xml', 'type' => 'string'), + 'Baz' => array( + 'type' => 'object', + 'location' => 'xml', + 'properties' => array( + 'Bar' => array('type' => 'string'), + 'Bam' => array() + ) + ) + ) + ), + array('Foo' => 'test', 'Baz' => array('Bar' => 'abc', 'Bam' => 'foo')), + 'testabcfoo' + ), + // Add an array that contains an object + array( + array( + 'parameters' => array( + 'Baz' => array( + 'type' => 'array', + 'location' => 'xml', + 'items' => array( + 'type' => 'object', + 'sentAs' => 'Bar', + 'properties' => array('A' => array(), 'B' => array()) + ) + ) + ) + ), + array('Baz' => array( + array('A' => '1', 'B' => '2'), + array('A' => '3', 'B' => '4') + )), + '1234' + ), + // Add an object of attributes + array( + array( + 'parameters' => array( + 'Foo' => array('location' => 'xml', 'type' => 'string'), + 'Baz' => array( + 'type' => 'object', + 'location' => 'xml', + 'properties' => array( + 'Bar' => array('type' => 'string', 'data' => array('xmlAttribute' => true)), + 'Bam' => array() + ) + ) + ) + ), + array('Foo' => 'test', 'Baz' => array('Bar' => 'abc', 'Bam' => 'foo')), + 'testfoo' + ), + // Check order doesn't matter + array( + array( + 'parameters' => array( + 'Foo' => array('location' => 'xml', 'type' => 'string'), + 'Baz' => array( + 'type' => 'object', + 'location' => 'xml', + 'properties' => array( + 'Bar' => array('type' => 'string', 'data' => array('xmlAttribute' => true)), + 'Bam' => array() + ) + ) + ) + ), + array('Foo' => 'test', 'Baz' => array('Bam' => 'foo', 'Bar' => 'abc')), + 'testfoo' + ), + // Add values with custom namespaces + array( + array( + 'parameters' => array( + 'Foo' => array( + 'location' => 'xml', + 'type' => 'string', + 'data' => array( + 'xmlNamespace' => 'http://foo.com' + ) + ) + ) + ), + array('Foo' => 'test'), + 'test' + ), + // Add attributes with custom namespace prefix + array( + array( + 'parameters' => array( + 'Wrap' => array( + 'type' => 'object', + 'location' => 'xml', + 'properties' => array( + 'Foo' => array( + 'type' => 'string', + 'sentAs' => 'xsi:baz', + 'data' => array( + 'xmlNamespace' => 'http://foo.com', + 'xmlAttribute' => true + ) + ) + ) + ), + ) + ), + array('Wrap' => array( + 'Foo' => 'test' + )), + '' + ), + // Add nodes with custom namespace prefix + array( + array( + 'parameters' => array( + 'Wrap' => array( + 'type' => 'object', + 'location' => 'xml', + 'properties' => array( + 'Foo' => array( + 'type' => 'string', + 'sentAs' => 'xsi:Foo', + 'data' => array( + 'xmlNamespace' => 'http://foobar.com' + ) + ) + ) + ), + ) + ), + array('Wrap' => array( + 'Foo' => 'test' + )), + 'test' + ), + array( + array( + 'parameters' => array( + 'Foo' => array( + 'location' => 'xml', + 'type' => 'string', + 'data' => array( + 'xmlNamespace' => 'http://foo.com' + ) + ) + ) + ), + array('Foo' => '

    This is a title

    '), + 'This is a title]]>' + ), + // Flat array at top level + array( + array( + 'parameters' => array( + 'Bars' => array( + 'type' => 'array', + 'data' => array('xmlFlattened' => true), + 'location' => 'xml', + 'items' => array( + 'type' => 'object', + 'sentAs' => 'Bar', + 'properties' => array( + 'A' => array(), + 'B' => array() + ) + ) + ), + 'Boos' => array( + 'type' => 'array', + 'data' => array('xmlFlattened' => true), + 'location' => 'xml', + 'items' => array( + 'sentAs' => 'Boo', + 'type' => 'string' + ) + ) + ) + ), + array( + 'Bars' => array( + array('A' => '1', 'B' => '2'), + array('A' => '3', 'B' => '4') + ), + 'Boos' => array('test', '123') + ), + '1234test123' + ), + // Nested flat arrays + array( + array( + 'parameters' => array( + 'Delete' => array( + 'type' => 'object', + 'location' => 'xml', + 'properties' => array( + 'Items' => array( + 'type' => 'array', + 'data' => array('xmlFlattened' => true), + 'items' => array( + 'type' => 'object', + 'sentAs' => 'Item', + 'properties' => array( + 'A' => array(), + 'B' => array() + ) + ) + ) + ) + ) + ) + ), + array( + 'Delete' => array( + 'Items' => array( + array('A' => '1', 'B' => '2'), + array('A' => '3', 'B' => '4') + ) + ) + ), + '1234' + ) + ); + } + + /** + * @dataProvider xmlProvider + */ + public function testSerializesXml(array $operation, array $input, $xml) + { + $operation = new Operation($operation); + $command = $this->getMockBuilder('Guzzle\Service\Command\OperationCommand') + ->setConstructorArgs(array($input, $operation)) + ->getMockForAbstractClass(); + $command->setClient(new Client('http://www.test.com/some/path.php')); + $request = $command->prepare(); + if (!empty($input)) { + $this->assertEquals('application/xml', (string) $request->getHeader('Content-Type')); + } else { + $this->assertNull($request->getHeader('Content-Type')); + } + $body = str_replace(array("\n", ""), '', (string) $request->getBody()); + $this->assertEquals($xml, $body); + } + + public function testAddsContentTypeAndTopLevelValues() + { + $operation = new Operation(array( + 'data' => array( + 'xmlRoot' => array( + 'name' => 'test', + 'namespaces' => array( + 'xsi' => 'http://foo.com' + ) + ) + ), + 'parameters' => array( + 'Foo' => array('location' => 'xml', 'type' => 'string'), + 'Baz' => array('location' => 'xml', 'type' => 'string') + ) + )); + + $command = $this->getMockBuilder('Guzzle\Service\Command\OperationCommand') + ->setConstructorArgs(array(array( + 'Foo' => 'test', + 'Baz' => 'bar' + ), $operation)) + ->getMockForAbstractClass(); + + $command->setClient(new Client()); + $request = $command->prepare(); + $this->assertEquals('application/xml', (string) $request->getHeader('Content-Type')); + $this->assertEquals( + '' . "\n" + . 'testbar' . "\n", + (string) $request->getBody() + ); + } + + public function testCanChangeContentType() + { + $visitor = new XmlVisitor(); + $visitor->setContentTypeHeader('application/foo'); + $this->assertEquals('application/foo', $this->readAttribute($visitor, 'contentType')); + } + + public function testCanAddArrayOfSimpleTypes() + { + $request = new EntityEnclosingRequest('POST', 'http://foo.com'); + $visitor = new XmlVisitor(); + $param = new Parameter(array( + 'type' => 'object', + 'location' => 'xml', + 'name' => 'Out', + 'properties' => array( + 'Nodes' => array( + 'required' => true, + 'type' => 'array', + 'min' => 1, + 'items' => array('type' => 'string', 'sentAs' => 'Node') + ) + ) + )); + + $param->setParent(new Operation(array( + 'data' => array( + 'xmlRoot' => array( + 'name' => 'Test', + 'namespaces' => array( + 'https://foo/' + ) + ) + ) + ))); + + $value = array('Nodes' => array('foo', 'baz')); + $this->assertTrue($this->validator->validate($param, $value)); + $visitor->visit($this->command, $request, $param, $value); + $visitor->after($this->command, $request); + + $this->assertEquals( + "\n" + . "foobaz\n", + (string) $request->getBody() + ); + } + + public function testCanAddMultipleNamespacesToRoot() + { + $operation = new Operation(array( + 'data' => array( + 'xmlRoot' => array( + 'name' => 'Hi', + 'namespaces' => array( + 'xsi' => 'http://foo.com', + 'foo' => 'http://foobar.com' + ) + ) + ), + 'parameters' => array( + 'Foo' => array('location' => 'xml', 'type' => 'string') + ) + )); + + $command = $this->getMockBuilder('Guzzle\Service\Command\OperationCommand') + ->setConstructorArgs(array(array( + 'Foo' => 'test' + ), $operation)) + ->getMockForAbstractClass(); + + $command->setClient(new Client()); + $request = $command->prepare(); + $this->assertEquals('application/xml', (string) $request->getHeader('Content-Type')); + $this->assertEquals( + '' . "\n" + . 'test' . "\n", + (string) $request->getBody() + ); + } + + public function testValuesAreFiltered() + { + $operation = new Operation(array( + 'parameters' => array( + 'Foo' => array( + 'location' => 'xml', + 'type' => 'string', + 'filters' => array('strtoupper') + ), + 'Bar' => array( + 'location' => 'xml', + 'type' => 'object', + 'properties' => array( + 'Baz' => array( + 'filters' => array('strtoupper') + ) + ) + ) + ) + )); + + $command = $this->getMockBuilder('Guzzle\Service\Command\OperationCommand') + ->setConstructorArgs(array(array( + 'Foo' => 'test', + 'Bar' => array( + 'Baz' => 'abc' + ) + ), $operation)) + ->getMockForAbstractClass(); + + $command->setClient(new Client()); + $request = $command->prepare(); + $this->assertEquals( + '' . "\n" + . 'TESTABC' . "\n", + (string) $request->getBody() + ); + } + + public function testSkipsNullValues() + { + $operation = new Operation(array( + 'parameters' => array( + 'Foo' => array( + 'location' => 'xml', + 'type' => 'string' + ), + 'Bar' => array( + 'location' => 'xml', + 'type' => 'object', + 'properties' => array( + 'Baz' => array(), + 'Bam' => array(), + ) + ), + 'Arr' => array( + 'type' => 'array', + 'items' => array( + 'type' => 'string' + ) + ) + ) + )); + + $command = $this->getMockBuilder('Guzzle\Service\Command\OperationCommand') + ->setConstructorArgs(array(array( + 'Foo' => null, + 'Bar' => array( + 'Bar' => null, + 'Bam' => 'test' + ), + 'Arr' => array(null) + ), $operation)) + ->getMockForAbstractClass(); + + $command->setClient(new Client()); + $request = $command->prepare(); + $this->assertEquals( + '' . "\n" + . 'test' . "\n", + (string) $request->getBody() + ); + } + + public function testAllowsXmlEncoding() + { + $operation = new Operation(array( + 'data' => array( + 'xmlEncoding' => 'UTF-8' + ), + 'parameters' => array( + 'Foo' => array('location' => 'xml') + ) + )); + $command = $this->getMockBuilder('Guzzle\Service\Command\OperationCommand') + ->setConstructorArgs(array(array('Foo' => 'test'), $operation)) + ->getMockForAbstractClass(); + $command->setClient(new Client()); + $request = $command->prepare(); + $this->assertEquals( + '' . "\n" + . 'test' . "\n", + (string) $request->getBody() + ); + } + + public function testAllowsSendingXmlPayloadIfNoXmlParamsWereSet() + { + $operation = new Operation(array( + 'httpMethod' => 'POST', + 'data' => array('xmlAllowEmpty' => true), + 'parameters' => array('Foo' => array('location' => 'xml')) + )); + $command = $this->getMockBuilder('Guzzle\Service\Command\OperationCommand') + ->setConstructorArgs(array(array(), $operation)) + ->getMockForAbstractClass(); + $command->setClient(new Client('http://foo.com')); + $request = $command->prepare(); + $this->assertEquals( + '' . "\n" + . '' . "\n", + (string) $request->getBody() + ); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Response/AbstractResponseVisitorTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Response/AbstractResponseVisitorTest.php new file mode 100644 index 000000000..7b8600342 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Response/AbstractResponseVisitorTest.php @@ -0,0 +1,29 @@ +value = array(); + $this->command = new MockCommand(); + $this->response = new Response(200, array( + 'X-Foo' => 'bar', + 'Content-Length' => 3, + 'Content-Type' => 'text/plain' + ), 'Foo'); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Response/BodyVisitorTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Response/BodyVisitorTest.php new file mode 100644 index 000000000..932e39bff --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Response/BodyVisitorTest.php @@ -0,0 +1,21 @@ + 'body', 'name' => 'foo')); + $visitor->visit($this->command, $this->response, $param, $this->value); + $this->assertEquals('Foo', (string) $this->value['foo']); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Response/HeaderVisitorTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Response/HeaderVisitorTest.php new file mode 100644 index 000000000..db54b1abb --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Response/HeaderVisitorTest.php @@ -0,0 +1,98 @@ + 'header', + 'name' => 'ContentType', + 'sentAs' => 'Content-Type' + )); + $visitor->visit($this->command, $this->response, $param, $this->value); + $this->assertEquals('text/plain', $this->value['ContentType']); + } + + public function testVisitsLocationWithFilters() + { + $visitor = new Visitor(); + $param = new Parameter(array( + 'location' => 'header', + 'name' => 'Content-Type', + 'filters' => array('strtoupper') + )); + $visitor->visit($this->command, $this->response, $param, $this->value); + $this->assertEquals('TEXT/PLAIN', $this->value['Content-Type']); + } + + public function testVisitsMappedPrefixHeaders() + { + $visitor = new Visitor(); + $param = new Parameter(array( + 'location' => 'header', + 'name' => 'Metadata', + 'sentAs' => 'X-Baz-', + 'type' => 'object', + 'additionalProperties' => array( + 'type' => 'string' + ) + )); + $response = new Response(200, array( + 'X-Baz-Test' => 'ABC', + 'X-Baz-Bar' => array('123', '456'), + 'Content-Length' => 3 + ), 'Foo'); + $visitor->visit($this->command, $response, $param, $this->value); + $this->assertEquals(array( + 'Metadata' => array( + 'Test' => 'ABC', + 'Bar' => array('123', '456') + ) + ), $this->value); + } + + /** + * @group issue-399 + * @link https://github.com/guzzle/guzzle/issues/399 + */ + public function testDiscardingUnknownHeaders() + { + $visitor = new Visitor(); + $param = new Parameter(array( + 'location' => 'header', + 'name' => 'Content-Type', + 'additionalParameters' => false + )); + $visitor->visit($this->command, $this->response, $param, $this->value); + $this->assertEquals('text/plain', $this->value['Content-Type']); + $this->assertArrayNotHasKey('X-Foo', $this->value); + } + + /** + * @group issue-399 + * @link https://github.com/guzzle/guzzle/issues/399 + */ + public function testDiscardingUnknownPropertiesWithAliasing() + { + $visitor = new Visitor(); + $param = new Parameter(array( + 'location' => 'header', + 'name' => 'ContentType', + 'sentAs' => 'Content-Type', + 'additionalParameters' => false + )); + $visitor->visit($this->command, $this->response, $param, $this->value); + $this->assertEquals('text/plain', $this->value['ContentType']); + $this->assertArrayNotHasKey('X-Foo', $this->value); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Response/JsonVisitorTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Response/JsonVisitorTest.php new file mode 100644 index 000000000..4f8d30b1e --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Response/JsonVisitorTest.php @@ -0,0 +1,157 @@ +getMockBuilder('Guzzle\Service\Command\AbstractCommand') + ->setMethods(array('getResponse')) + ->getMockForAbstractClass(); + $command->expects($this->once()) + ->method('getResponse') + ->will($this->returnValue(new Response(200, null, '{"foo":"bar"}'))); + $result = array(); + $visitor->before($command, $result); + $this->assertEquals(array('foo' => 'bar'), $result); + } + + public function testVisitsLocation() + { + $visitor = new Visitor(); + $param = new Parameter(array( + 'name' => 'foo', + 'type' => 'array', + 'items' => array( + 'filters' => 'strtoupper', + 'type' => 'string' + ) + )); + $this->value = array('foo' => array('a', 'b', 'c')); + $visitor->visit($this->command, $this->response, $param, $this->value); + $this->assertEquals(array('A', 'B', 'C'), $this->value['foo']); + } + + public function testRenamesTopLevelValues() + { + $visitor = new Visitor(); + $param = new Parameter(array( + 'name' => 'foo', + 'sentAs' => 'Baz', + 'type' => 'string', + )); + $this->value = array('Baz' => 'test'); + $visitor->visit($this->command, $this->response, $param, $this->value); + $this->assertEquals(array('foo' => 'test'), $this->value); + } + + public function testRenamesDoesNotFailForNonExistentKey() + { + $visitor = new Visitor(); + $param = new Parameter(array( + 'name' => 'foo', + 'type' => 'object', + 'properties' => array( + 'bar' => array( + 'name' => 'bar', + 'sentAs' => 'baz', + ), + ), + )); + $this->value = array('foo' => array('unknown' => 'Unknown')); + $visitor->visit($this->command, $this->response, $param, $this->value); + $this->assertEquals(array('foo' => array('unknown' => 'Unknown')), $this->value); + } + + public function testTraversesObjectsAndAppliesFilters() + { + $visitor = new Visitor(); + $param = new Parameter(array( + 'name' => 'foo', + 'type' => 'object', + 'properties' => array( + 'foo' => array('filters' => 'strtoupper'), + 'bar' => array('filters' => 'strtolower') + ) + )); + $this->value = array('foo' => array('foo' => 'hello', 'bar' => 'THERE')); + $visitor->visit($this->command, $this->response, $param, $this->value); + $this->assertEquals(array('foo' => 'HELLO', 'bar' => 'there'), $this->value['foo']); + } + + /** + * @group issue-399 + * @link https://github.com/guzzle/guzzle/issues/399 + */ + public function testDiscardingUnknownProperties() + { + $visitor = new Visitor(); + $param = new Parameter(array( + 'name' => 'foo', + 'type' => 'object', + 'additionalProperties' => false, + 'properties' => array( + 'bar' => array( + 'type' => 'string', + 'name' => 'bar', + ), + ), + )); + $this->value = array('foo' => array('bar' => 15, 'unknown' => 'Unknown')); + $visitor->visit($this->command, $this->response, $param, $this->value); + $this->assertEquals(array('foo' => array('bar' => 15)), $this->value); + } + + /** + * @group issue-399 + * @link https://github.com/guzzle/guzzle/issues/399 + */ + public function testDiscardingUnknownPropertiesWithAliasing() + { + $visitor = new Visitor(); + $param = new Parameter(array( + 'name' => 'foo', + 'type' => 'object', + 'additionalProperties' => false, + 'properties' => array( + 'bar' => array( + 'name' => 'bar', + 'sentAs' => 'baz', + ), + ), + )); + $this->value = array('foo' => array('baz' => 15, 'unknown' => 'Unknown')); + $visitor->visit($this->command, $this->response, $param, $this->value); + $this->assertEquals(array('foo' => array('bar' => 15)), $this->value); + } + + public function testWalksAdditionalProperties() + { + $visitor = new Visitor(); + $param = new Parameter(array( + 'name' => 'foo', + 'type' => 'object', + 'additionalProperties' => array( + 'type' => 'object', + 'properties' => array( + 'bar' => array( + 'type' => 'string', + 'filters' => array('base64_decode') + ) + ), + ), + )); + $this->value = array('foo' => array('baz' => array('bar' => 'Zm9v'))); + $visitor->visit($this->command, $this->response, $param, $this->value); + $this->assertEquals('foo', $this->value['foo']['baz']['bar']); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Response/ReasonPhraseVisitorTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Response/ReasonPhraseVisitorTest.php new file mode 100644 index 000000000..23cd40fed --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Response/ReasonPhraseVisitorTest.php @@ -0,0 +1,21 @@ + 'reasonPhrase', 'name' => 'phrase')); + $visitor->visit($this->command, $this->response, $param, $this->value); + $this->assertEquals('OK', $this->value['phrase']); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Response/StatusCodeVisitorTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Response/StatusCodeVisitorTest.php new file mode 100644 index 000000000..7211a5801 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Response/StatusCodeVisitorTest.php @@ -0,0 +1,21 @@ + 'statusCode', 'name' => 'code')); + $visitor->visit($this->command, $this->response, $param, $this->value); + $this->assertEquals(200, $this->value['code']); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Response/XmlVisitorTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Response/XmlVisitorTest.php new file mode 100644 index 000000000..e7acc3247 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/Response/XmlVisitorTest.php @@ -0,0 +1,415 @@ +getMockBuilder('Guzzle\Service\Command\AbstractCommand') + ->setMethods(array('getResponse')) + ->getMockForAbstractClass(); + $command->expects($this->once()) + ->method('getResponse') + ->will($this->returnValue(new Response(200, null, 'test'))); + $result = array(); + $visitor->before($command, $result); + $this->assertEquals(array('Bar' => 'test'), $result); + } + + public function testBeforeMethodParsesXmlWithNamespace() + { + $this->markTestSkipped("Response/XmlVisitor cannot accept 'xmlns' in response, see #368 (http://git.io/USa1mA)."); + + $visitor = new Visitor(); + $command = $this->getMockBuilder('Guzzle\Service\Command\AbstractCommand') + ->setMethods(array('getResponse')) + ->getMockForAbstractClass(); + $command->expects($this->once()) + ->method('getResponse') + ->will($this->returnValue(new Response(200, null, 'test'))); + $result = array(); + $visitor->before($command, $result); + $this->assertEquals(array('Bar' => 'test'), $result); + } + + public function testBeforeMethodParsesNestedXml() + { + $visitor = new Visitor(); + $command = $this->getMockBuilder('Guzzle\Service\Command\AbstractCommand') + ->setMethods(array('getResponse')) + ->getMockForAbstractClass(); + $command->expects($this->once()) + ->method('getResponse') + ->will($this->returnValue(new Response(200, null, 'test'))); + $result = array(); + $visitor->before($command, $result); + $this->assertEquals(array('Items' => array('Bar' => 'test')), $result); + } + + public function testCanExtractAndRenameTopLevelXmlValues() + { + $visitor = new Visitor(); + $param = new Parameter(array( + 'location' => 'xml', + 'name' => 'foo', + 'sentAs' => 'Bar' + )); + $value = array('Bar' => 'test'); + $visitor->visit($this->command, $this->response, $param, $value); + $this->assertArrayHasKey('foo', $value); + $this->assertEquals('test', $value['foo']); + } + + public function testEnsuresRepeatedArraysAreInCorrectLocations() + { + $visitor = new Visitor(); + $param = new Parameter(array( + 'location' => 'xml', + 'name' => 'foo', + 'sentAs' => 'Foo', + 'type' => 'array', + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'Bar' => array('type' => 'string'), + 'Baz' => array('type' => 'string'), + 'Bam' => array('type' => 'string') + ) + ) + )); + + $xml = new \SimpleXMLElement('12'); + $value = json_decode(json_encode($xml), true); + $visitor->visit($this->command, $this->response, $param, $value); + $this->assertEquals(array( + 'foo' => array( + array ( + 'Bar' => '1', + 'Baz' => '2' + ) + ) + ), $value); + } + + public function testEnsuresFlatArraysAreFlat() + { + $visitor = new Visitor(); + $param = new Parameter(array( + 'location' => 'xml', + 'name' => 'foo', + 'type' => 'array', + 'items' => array('type' => 'string') + )); + + $value = array('foo' => array('bar', 'baz')); + $visitor->visit($this->command, $this->response, $param, $value); + $this->assertEquals(array('foo' => array('bar', 'baz')), $value); + + $value = array('foo' => 'bar'); + $visitor->visit($this->command, $this->response, $param, $value); + $this->assertEquals(array('foo' => array('bar')), $value); + } + + public function xmlDataProvider() + { + $param = new Parameter(array( + 'location' => 'xml', + 'name' => 'Items', + 'type' => 'array', + 'items' => array( + 'type' => 'object', + 'name' => 'Item', + 'properties' => array( + 'Bar' => array('type' => 'string'), + 'Baz' => array('type' => 'string') + ) + ) + )); + + return array( + array($param, '12', array( + 'Items' => array( + array('Bar' => 1), + array('Bar' => 2) + ) + )), + array($param, '1', array( + 'Items' => array( + array('Bar' => 1) + ) + )), + array($param, '', array( + 'Items' => array() + )) + ); + } + + /** + * @dataProvider xmlDataProvider + */ + public function testEnsuresWrappedArraysAreInCorrectLocations($param, $xml, $result) + { + $visitor = new Visitor(); + $xml = new \SimpleXMLElement($xml); + $value = json_decode(json_encode($xml), true); + $visitor->visit($this->command, $this->response, $param, $value); + $this->assertEquals($result, $value); + } + + public function testCanRenameValues() + { + $visitor = new Visitor(); + $param = new Parameter(array( + 'name' => 'TerminatingInstances', + 'type' => 'array', + 'location' => 'xml', + 'sentAs' => 'instancesSet', + 'items' => array( + 'name' => 'item', + 'type' => 'object', + 'sentAs' => 'item', + 'properties' => array( + 'InstanceId' => array( + 'type' => 'string', + 'sentAs' => 'instanceId', + ), + 'CurrentState' => array( + 'type' => 'object', + 'sentAs' => 'currentState', + 'properties' => array( + 'Code' => array( + 'type' => 'numeric', + 'sentAs' => 'code', + ), + 'Name' => array( + 'type' => 'string', + 'sentAs' => 'name', + ), + ), + ), + 'PreviousState' => array( + 'type' => 'object', + 'sentAs' => 'previousState', + 'properties' => array( + 'Code' => array( + 'type' => 'numeric', + 'sentAs' => 'code', + ), + 'Name' => array( + 'type' => 'string', + 'sentAs' => 'name', + ), + ), + ), + ), + ) + )); + + $value = array( + 'instancesSet' => array ( + 'item' => array ( + 'instanceId' => 'i-3ea74257', + 'currentState' => array( + 'code' => '32', + 'name' => 'shutting-down', + ), + 'previousState' => array( + 'code' => '16', + 'name' => 'running', + ), + ), + ) + ); + + $visitor->visit($this->command, $this->response, $param, $value); + + $this->assertEquals(array( + 'TerminatingInstances' => array( + array( + 'InstanceId' => 'i-3ea74257', + 'CurrentState' => array( + 'Code' => '32', + 'Name' => 'shutting-down', + ), + 'PreviousState' => array( + 'Code' => '16', + 'Name' => 'running', + ) + ) + ) + ), $value); + } + + public function testCanRenameAttributes() + { + $visitor = new Visitor(); + $param = new Parameter(array( + 'name' => 'RunningQueues', + 'type' => 'array', + 'location' => 'xml', + 'items' => array( + 'type' => 'object', + 'sentAs' => 'item', + 'properties' => array( + 'QueueId' => array( + 'type' => 'string', + 'sentAs' => 'queue_id', + 'data' => array( + 'xmlAttribute' => true, + ), + ), + 'CurrentState' => array( + 'type' => 'object', + 'properties' => array( + 'Code' => array( + 'type' => 'numeric', + 'sentAs' => 'code', + 'data' => array( + 'xmlAttribute' => true, + ), + ), + 'Name' => array( + 'sentAs' => 'name', + 'data' => array( + 'xmlAttribute' => true, + ), + ), + ), + ), + 'PreviousState' => array( + 'type' => 'object', + 'properties' => array( + 'Code' => array( + 'type' => 'numeric', + 'sentAs' => 'code', + 'data' => array( + 'xmlAttribute' => true, + ), + ), + 'Name' => array( + 'sentAs' => 'name', + 'data' => array( + 'xmlAttribute' => true, + ), + ), + ), + ), + ), + ) + )); + + $xml = ''; + $value = json_decode(json_encode(new \SimpleXMLElement($xml)), true); + $visitor->visit($this->command, $this->response, $param, $value); + + $this->assertEquals(array( + 'RunningQueues' => array( + array( + 'QueueId' => 'q-3ea74257', + 'CurrentState' => array( + 'Code' => '32', + 'Name' => 'processing', + ), + 'PreviousState' => array( + 'Code' => '16', + 'Name' => 'wait', + ), + ), + ) + ), $value); + } + + public function testAddsEmptyArraysWhenValueIsMissing() + { + $visitor = new Visitor(); + $param = new Parameter(array( + 'name' => 'Foo', + 'type' => 'array', + 'location' => 'xml', + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'Baz' => array('type' => 'array'), + 'Bar' => array( + 'type' => 'object', + 'properties' => array( + 'Baz' => array('type' => 'array'), + ) + ) + ) + ) + )); + + $value = array(); + $visitor->visit($this->command, $this->response, $param, $value); + + $value = array( + 'Foo' => array( + 'Bar' => array() + ) + ); + $visitor->visit($this->command, $this->response, $param, $value); + $this->assertEquals(array( + 'Foo' => array( + array( + 'Bar' => array() + ) + ) + ), $value); + } + + /** + * @group issue-399 + * @link https://github.com/guzzle/guzzle/issues/399 + */ + public function testDiscardingUnknownProperties() + { + $visitor = new Visitor(); + $param = new Parameter(array( + 'name' => 'foo', + 'type' => 'object', + 'additionalProperties' => false, + 'properties' => array( + 'bar' => array( + 'type' => 'string', + 'name' => 'bar', + ), + ), + )); + $this->value = array('foo' => array('bar' => 15, 'unknown' => 'Unknown')); + $visitor->visit($this->command, $this->response, $param, $this->value); + $this->assertEquals(array('foo' => array('bar' => 15)), $this->value); + } + + /** + * @group issue-399 + * @link https://github.com/guzzle/guzzle/issues/399 + */ + public function testDiscardingUnknownPropertiesWithAliasing() + { + $visitor = new Visitor(); + $param = new Parameter(array( + 'name' => 'foo', + 'type' => 'object', + 'additionalProperties' => false, + 'properties' => array( + 'bar' => array( + 'name' => 'bar', + 'sentAs' => 'baz', + ), + ), + )); + $this->value = array('foo' => array('baz' => 15, 'unknown' => 'Unknown')); + $visitor->visit($this->command, $this->response, $param, $this->value); + $this->assertEquals(array('foo' => array('bar' => 15)), $this->value); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/VisitorFlyweightTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/VisitorFlyweightTest.php new file mode 100644 index 000000000..a252ffe60 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/LocationVisitor/VisitorFlyweightTest.php @@ -0,0 +1,53 @@ +assertInstanceOf('Guzzle\Service\Command\LocationVisitor\Request\JsonVisitor', $f->getRequestVisitor('json')); + $this->assertInstanceOf('Guzzle\Service\Command\LocationVisitor\Response\JsonVisitor', $f->getResponseVisitor('json')); + } + + public function testCanUseCustomMappings() + { + $f = new VisitorFlyweight(array()); + $this->assertEquals(array(), $this->readAttribute($f, 'mappings')); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage No request visitor has been mapped for foo + */ + public function testThrowsExceptionWhenRetrievingUnknownVisitor() + { + VisitorFlyweight::getInstance()->getRequestVisitor('foo'); + } + + public function testCachesVisitors() + { + $f = new VisitorFlyweight(); + $v1 = $f->getRequestVisitor('json'); + $this->assertSame($v1, $f->getRequestVisitor('json')); + } + + public function testAllowsAddingVisitors() + { + $f = new VisitorFlyweight(); + $j1 = new JsonRequestVisitor(); + $j2 = new JsonResponseVisitor(); + $f->addRequestVisitor('json', $j1); + $f->addResponseVisitor('json', $j2); + $this->assertSame($j1, $f->getRequestVisitor('json')); + $this->assertSame($j2, $f->getResponseVisitor('json')); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/OperationCommandTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/OperationCommandTest.php new file mode 100644 index 000000000..95fb533af --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/OperationCommandTest.php @@ -0,0 +1,102 @@ +getRequestSerializer(); + $b = new DefaultRequestSerializer(VisitorFlyweight::getInstance()); + $operation->setRequestSerializer($b); + $this->assertNotSame($a, $operation->getRequestSerializer()); + } + + public function testPreparesRequestUsingSerializer() + { + $op = new OperationCommand(array(), new Operation()); + $op->setClient(new Client()); + $s = $this->getMockBuilder('Guzzle\Service\Command\RequestSerializerInterface') + ->setMethods(array('prepare')) + ->getMockForAbstractClass(); + $s->expects($this->once()) + ->method('prepare') + ->will($this->returnValue(new EntityEnclosingRequest('POST', 'http://foo.com'))); + $op->setRequestSerializer($s); + $op->prepare(); + } + + public function testParsesResponsesWithResponseParser() + { + $op = new OperationCommand(array(), new Operation()); + $p = $this->getMockBuilder('Guzzle\Service\Command\ResponseParserInterface') + ->setMethods(array('parse')) + ->getMockForAbstractClass(); + $p->expects($this->once()) + ->method('parse') + ->will($this->returnValue(array('foo' => 'bar'))); + $op->setResponseParser($p); + $op->setClient(new Client()); + $request = $op->prepare(); + $request->setResponse(new Response(200), true); + $this->assertEquals(array('foo' => 'bar'), $op->execute()); + } + + public function testParsesResponsesUsingModelParserWhenMatchingModelIsFound() + { + $description = ServiceDescription::factory(array( + 'operations' => array( + 'foo' => array('responseClass' => 'bar', 'responseType' => 'model') + ), + 'models' => array( + 'bar' => array( + 'type' => 'object', + 'properties' => array( + 'Baz' => array('type' => 'string', 'location' => 'xml') + ) + ) + ) + )); + + $op = new OperationCommand(array(), $description->getOperation('foo')); + $op->setClient(new Client()); + $request = $op->prepare(); + $request->setResponse(new Response(200, array( + 'Content-Type' => 'application/xml' + ), 'Bar'), true); + $result = $op->execute(); + $this->assertEquals(new Model(array('Baz' => 'Bar')), $result); + } + + public function testAllowsRawResponses() + { + $description = new ServiceDescription(array( + 'operations' => array('foo' => array('responseClass' => 'bar', 'responseType' => 'model')), + 'models' => array('bar' => array()) + )); + $op = new OperationCommand(array( + OperationCommand::RESPONSE_PROCESSING => OperationCommand::TYPE_RAW + ), $description->getOperation('foo')); + $op->setClient(new Client()); + $request = $op->prepare(); + $response = new Response(200, array( + 'Content-Type' => 'application/xml' + ), 'Bar'); + $request->setResponse($response, true); + $this->assertSame($response, $op->execute()); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/OperationResponseParserTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/OperationResponseParserTest.php new file mode 100644 index 000000000..69ba1fcea --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Service/Command/OperationResponseParserTest.php @@ -0,0 +1,335 @@ +addVisitor('foo', $visitor); + $this->assertSame($visitor, $this->readAttribute($p, 'factory')->getResponseVisitor('foo')); + } + + public function testUsesParentParser() + { + $p = new OperationResponseParser(new VisitorFlyweight()); + $operation = new Operation(); + $operation->setServiceDescription(new ServiceDescription()); + $op = new OperationCommand(array(), $operation); + $op->setResponseParser($p)->setClient(new Client()); + $op->prepare()->setResponse(new Response(200, array('Content-Type' => 'application/xml'), 'C'), true); + $this->assertInstanceOf('SimpleXMLElement', $op->execute()); + } + + public function testVisitsLocations() + { + $parser = new OperationResponseParser(new VisitorFlyweight(array())); + $parser->addVisitor('statusCode', new StatusCodeVisitor()); + $parser->addVisitor('reasonPhrase', new ReasonPhraseVisitor()); + $parser->addVisitor('json', new JsonVisitor()); + $op = new OperationCommand(array(), $this->getDescription()->getOperation('test')); + $op->setResponseParser($parser)->setClient(new Client()); + $op->prepare()->setResponse(new Response(201), true); + $result = $op->execute(); + $this->assertEquals(201, $result['code']); + $this->assertEquals('Created', $result['phrase']); + } + + public function testVisitsLocationsForJsonResponse() + { + $parser = OperationResponseParser::getInstance(); + $operation = $this->getDescription()->getOperation('test'); + $op = new OperationCommand(array(), $operation); + $op->setResponseParser($parser)->setClient(new Client()); + $op->prepare()->setResponse(new Response(200, array( + 'Content-Type' => 'application/json' + ), '{"baz":"bar","enigma":"123"}'), true); + $result = $op->execute(); + $this->assertEquals(array( + 'baz' => 'bar', + 'enigma' => '123', + 'code' => 200, + 'phrase' => 'OK' + ), $result->toArray()); + } + + public function testSkipsUnkownModels() + { + $parser = OperationResponseParser::getInstance(); + $operation = $this->getDescription()->getOperation('test'); + $operation->setResponseClass('Baz')->setResponseType('model'); + $op = new OperationCommand(array(), $operation); + $op->setResponseParser($parser)->setClient(new Client()); + $op->prepare()->setResponse(new Response(201), true); + $this->assertInstanceOf('Guzzle\Http\Message\Response', $op->execute()); + } + + public function testAllowsModelProcessingToBeDisabled() + { + $parser = OperationResponseParser::getInstance(); + $operation = $this->getDescription()->getOperation('test'); + $op = new OperationCommand(array('command.response_processing' => 'native'), $operation); + $op->setResponseParser($parser)->setClient(new Client()); + $op->prepare()->setResponse(new Response(200, array( + 'Content-Type' => 'application/json' + ), '{"baz":"bar","enigma":"123"}'), true); + $result = $op->execute(); + $this->assertInstanceOf('Guzzle\Service\Resource\Model', $result); + $this->assertEquals(array( + 'baz' => 'bar', + 'enigma' => '123' + ), $result->toArray()); + } + + public function testCanInjectModelSchemaIntoModels() + { + $parser = new OperationResponseParser(VisitorFlyweight::getInstance(), true); + $desc = $this->getDescription(); + $operation = $desc->getOperation('test'); + $op = new OperationCommand(array(), $operation); + $op->setResponseParser($parser)->setClient(new Client()); + $op->prepare()->setResponse(new Response(200, array( + 'Content-Type' => 'application/json' + ), '{"baz":"bar","enigma":"123"}'), true); + $result = $op->execute(); + $this->assertSame($result->getStructure(), $desc->getModel('Foo')); + } + + public function testDoesNotParseXmlWhenNotUsingXmlVisitor() + { + $parser = OperationResponseParser::getInstance(); + $description = ServiceDescription::factory(array( + 'operations' => array('test' => array('responseClass' => 'Foo')), + 'models' => array( + 'Foo' => array( + 'type' => 'object', + 'properties' => array('baz' => array('location' => 'body')) + ) + ) + )); + $operation = $description->getOperation('test'); + $op = new OperationCommand(array(), $operation); + $op->setResponseParser($parser)->setClient(new Client()); + $brokenXml = '<><><>>>>'; + $op->prepare()->setResponse(new Response(200, array( + 'Content-Type' => 'application/xml' + ), $brokenXml), true); + $result = $op->execute(); + $this->assertEquals(array('baz'), $result->getKeys()); + $this->assertEquals($brokenXml, (string) $result['baz']); + } + + public function testVisitsAdditionalProperties() + { + $parser = OperationResponseParser::getInstance(); + $description = ServiceDescription::factory(array( + 'operations' => array('test' => array('responseClass' => 'Foo')), + 'models' => array( + 'Foo' => array( + 'type' => 'object', + 'properties' => array( + 'code' => array('location' => 'statusCode') + ), + 'additionalProperties' => array( + 'location' => 'json', + 'type' => 'object', + 'properties' => array( + 'a' => array( + 'type' => 'string', + 'filters' => 'strtoupper' + ) + ) + ) + ) + ) + )); + + $operation = $description->getOperation('test'); + $op = new OperationCommand(array(), $operation); + $op->setResponseParser($parser)->setClient(new Client()); + $json = '[{"a":"test"},{"a":"baz"}]'; + $op->prepare()->setResponse(new Response(200, array('Content-Type' => 'application/json'), $json), true); + $result = $op->execute()->toArray(); + $this->assertEquals(array( + 'code' => 200, + array('a' => 'TEST'), + array('a' => 'BAZ') + ), $result); + } + + /** + * @group issue-399 + * @link https://github.com/guzzle/guzzle/issues/399 + */ + public function testAdditionalPropertiesDisabledDiscardsData() + { + $parser = OperationResponseParser::getInstance(); + $description = ServiceDescription::factory(array( + 'operations' => array('test' => array('responseClass' => 'Foo')), + 'models' => array( + 'Foo' => array( + 'type' => 'object', + 'additionalProperties' => false, + 'properties' => array( + 'name' => array( + 'location' => 'json', + 'type' => 'string', + ), + 'nested' => array( + 'location' => 'json', + 'type' => 'object', + 'additionalProperties' => false, + 'properties' => array( + 'width' => array( + 'type' => 'integer' + ) + ), + ), + 'code' => array('location' => 'statusCode') + ), + + ) + ) + )); + + $operation = $description->getOperation('test'); + $op = new OperationCommand(array(), $operation); + $op->setResponseParser($parser)->setClient(new Client()); + $json = '{"name":"test", "volume":2.0, "nested":{"width":10,"bogus":1}}'; + $op->prepare()->setResponse(new Response(200, array('Content-Type' => 'application/json'), $json), true); + $result = $op->execute()->toArray(); + $this->assertEquals(array( + 'name' => 'test', + 'nested' => array( + 'width' => 10, + ), + 'code' => 200 + ), $result); + } + + public function testCreatesCustomResponseClassInterface() + { + $parser = OperationResponseParser::getInstance(); + $description = ServiceDescription::factory(array( + 'operations' => array('test' => array('responseClass' => 'Guzzle\Tests\Mock\CustomResponseModel')) + )); + $operation = $description->getOperation('test'); + $op = new OperationCommand(array(), $operation); + $op->setResponseParser($parser)->setClient(new Client()); + $op->prepare()->setResponse(new Response(200, array('Content-Type' => 'application/json'), 'hi!'), true); + $result = $op->execute(); + $this->assertInstanceOf('Guzzle\Tests\Mock\CustomResponseModel', $result); + $this->assertSame($op, $result->command); + } + + /** + * @expectedException \Guzzle\Service\Exception\ResponseClassException + * @expectedExceptionMessage must exist + */ + public function testEnsuresResponseClassExists() + { + $parser = OperationResponseParser::getInstance(); + $description = ServiceDescription::factory(array( + 'operations' => array('test' => array('responseClass' => 'Foo\Baz\Bar')) + )); + $operation = $description->getOperation('test'); + $op = new OperationCommand(array(), $operation); + $op->setResponseParser($parser)->setClient(new Client()); + $op->prepare()->setResponse(new Response(200, array('Content-Type' => 'application/json'), 'hi!'), true); + $op->execute(); + } + + /** + * @expectedException \Guzzle\Service\Exception\ResponseClassException + * @expectedExceptionMessage and implement + */ + public function testEnsuresResponseClassImplementsResponseClassInterface() + { + $parser = OperationResponseParser::getInstance(); + $description = ServiceDescription::factory(array( + 'operations' => array('test' => array('responseClass' => __CLASS__)) + )); + $operation = $description->getOperation('test'); + $op = new OperationCommand(array(), $operation); + $op->setResponseParser($parser)->setClient(new Client()); + $op->prepare()->setResponse(new Response(200, array('Content-Type' => 'application/json'), 'hi!'), true); + $op->execute(); + } + + protected function getDescription() + { + return ServiceDescription::factory(array( + 'operations' => array('test' => array('responseClass' => 'Foo')), + 'models' => array( + 'Foo' => array( + 'type' => 'object', + 'properties' => array( + 'baz' => array('type' => 'string', 'location' => 'json'), + 'code' => array('location' => 'statusCode'), + 'phrase' => array('location' => 'reasonPhrase'), + ) + ) + ) + )); + } + + public function testCanAddListenerToParseDomainObjects() + { + $client = new Client(); + $client->setDescription(ServiceDescription::factory(array( + 'operations' => array('test' => array('responseClass' => 'FooBazBar')) + ))); + $foo = new \stdClass(); + $client->getEventDispatcher()->addListener('command.parse_response', function ($e) use ($foo) { + $e['result'] = $foo; + }); + $command = $client->getCommand('test'); + $command->prepare()->setResponse(new Response(200), true); + $result = $command->execute(); + $this->assertSame($result, $foo); + } + + /** + * @group issue-399 + * @link https://github.com/guzzle/guzzle/issues/501 + */ + public function testAdditionalPropertiesWithRefAreResolved() + { + $parser = OperationResponseParser::getInstance(); + $description = ServiceDescription::factory(array( + 'operations' => array('test' => array('responseClass' => 'Foo')), + 'models' => array( + 'Baz' => array('type' => 'string'), + 'Foo' => array( + 'type' => 'object', + 'additionalProperties' => array('$ref' => 'Baz', 'location' => 'json') + ) + ) + )); + $operation = $description->getOperation('test'); + $op = new OperationCommand(array(), $operation); + $op->setResponseParser($parser)->setClient(new Client()); + $json = '{"a":"a","b":"b","c":"c"}'; + $op->prepare()->setResponse(new Response(200, array('Content-Type' => 'application/json'), $json), true); + $result = $op->execute()->toArray(); + $this->assertEquals(array('a' => 'a', 'b' => 'b', 'c' => 'c'), $result); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Service/Description/OperationTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Service/Description/OperationTest.php new file mode 100644 index 000000000..ae33b6925 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Service/Description/OperationTest.php @@ -0,0 +1,308 @@ + 'test', + 'summary' => 'doc', + 'notes' => 'notes', + 'documentationUrl' => 'http://www.example.com', + 'httpMethod' => 'POST', + 'uri' => '/api/v1', + 'responseClass' => 'array', + 'responseNotes' => 'returns the json_decoded response', + 'deprecated' => true, + 'parameters' => array( + 'key' => array( + 'required' => true, + 'type' => 'string', + 'maxLength' => 10 + ), + 'key_2' => array( + 'required' => true, + 'type' => 'integer', + 'default' => 10 + ) + ) + )); + + $this->assertEquals('test', $c->getName()); + $this->assertEquals('doc', $c->getSummary()); + $this->assertEquals('http://www.example.com', $c->getDocumentationUrl()); + $this->assertEquals('POST', $c->getHttpMethod()); + $this->assertEquals('/api/v1', $c->getUri()); + $this->assertEquals('array', $c->getResponseClass()); + $this->assertEquals('returns the json_decoded response', $c->getResponseNotes()); + $this->assertTrue($c->getDeprecated()); + $this->assertEquals('Guzzle\\Service\\Command\\OperationCommand', $c->getClass()); + $this->assertEquals(array( + 'key' => new Parameter(array( + 'name' => 'key', + 'required' => true, + 'type' => 'string', + 'maxLength' => 10, + 'parent' => $c + )), + 'key_2' => new Parameter(array( + 'name' => 'key_2', + 'required' => true, + 'type' => 'integer', + 'default' => 10, + 'parent' => $c + )) + ), $c->getParams()); + + $this->assertEquals(new Parameter(array( + 'name' => 'key_2', + 'required' => true, + 'type' => 'integer', + 'default' => 10, + 'parent' => $c + )), $c->getParam('key_2')); + + $this->assertNull($c->getParam('afefwef')); + $this->assertArrayNotHasKey('parent', $c->getParam('key_2')->toArray()); + } + + public function testAllowsConcreteCommands() + { + $c = new Operation(array( + 'name' => 'test', + 'class' => 'Guzzle\\Service\\Command\ClosureCommand', + 'parameters' => array( + 'p' => new Parameter(array( + 'name' => 'foo' + )) + ) + )); + $this->assertEquals('Guzzle\\Service\\Command\ClosureCommand', $c->getClass()); + } + + public function testConvertsToArray() + { + $data = array( + 'name' => 'test', + 'class' => 'Guzzle\\Service\\Command\ClosureCommand', + 'summary' => 'test', + 'documentationUrl' => 'http://www.example.com', + 'httpMethod' => 'PUT', + 'uri' => '/', + 'parameters' => array('p' => array('name' => 'foo')) + ); + $c = new Operation($data); + $toArray = $c->toArray(); + unset($data['name']); + $this->assertArrayHasKey('parameters', $toArray); + $this->assertInternalType('array', $toArray['parameters']); + + // Normalize the array + unset($data['parameters']); + unset($toArray['parameters']); + + $data['responseType'] = 'primitive'; + $data['responseClass'] = 'array'; + $this->assertEquals($data, $toArray); + } + + public function testDeterminesIfHasParam() + { + $command = $this->getTestCommand(); + $this->assertTrue($command->hasParam('data')); + $this->assertFalse($command->hasParam('baz')); + } + + public function testReturnsParamNames() + { + $command = $this->getTestCommand(); + $this->assertEquals(array('data'), $command->getParamNames()); + } + + protected function getTestCommand() + { + return new Operation(array( + 'parameters' => array( + 'data' => new Parameter(array( + 'type' => 'string' + )) + ) + )); + } + + public function testCanBuildUpCommands() + { + $c = new Operation(array()); + $c->setName('foo') + ->setClass('Baz') + ->setDeprecated(false) + ->setSummary('summary') + ->setDocumentationUrl('http://www.foo.com') + ->setHttpMethod('PUT') + ->setResponseNotes('oh') + ->setResponseClass('string') + ->setUri('/foo/bar') + ->addParam(new Parameter(array( + 'name' => 'test' + ))); + + $this->assertEquals('foo', $c->getName()); + $this->assertEquals('Baz', $c->getClass()); + $this->assertEquals(false, $c->getDeprecated()); + $this->assertEquals('summary', $c->getSummary()); + $this->assertEquals('http://www.foo.com', $c->getDocumentationUrl()); + $this->assertEquals('PUT', $c->getHttpMethod()); + $this->assertEquals('oh', $c->getResponseNotes()); + $this->assertEquals('string', $c->getResponseClass()); + $this->assertEquals('/foo/bar', $c->getUri()); + $this->assertEquals(array('test'), $c->getParamNames()); + } + + public function testCanRemoveParams() + { + $c = new Operation(array()); + $c->addParam(new Parameter(array('name' => 'foo'))); + $this->assertTrue($c->hasParam('foo')); + $c->removeParam('foo'); + $this->assertFalse($c->hasParam('foo')); + } + + public function testAddsNameToParametersIfNeeded() + { + $command = new Operation(array('parameters' => array('foo' => new Parameter(array())))); + $this->assertEquals('foo', $command->getParam('foo')->getName()); + } + + public function testContainsApiErrorInformation() + { + $command = $this->getOperation(); + $this->assertEquals(1, count($command->getErrorResponses())); + $arr = $command->toArray(); + $this->assertEquals(1, count($arr['errorResponses'])); + $command->addErrorResponse(400, 'Foo', 'Baz\\Bar'); + $this->assertEquals(2, count($command->getErrorResponses())); + $command->setErrorResponses(array()); + $this->assertEquals(0, count($command->getErrorResponses())); + } + + public function testHasNotes() + { + $o = new Operation(array('notes' => 'foo')); + $this->assertEquals('foo', $o->getNotes()); + $o->setNotes('bar'); + $this->assertEquals('bar', $o->getNotes()); + } + + public function testHasData() + { + $o = new Operation(array('data' => array('foo' => 'baz', 'bar' => 123))); + $o->setData('test', false); + $this->assertEquals('baz', $o->getData('foo')); + $this->assertEquals(123, $o->getData('bar')); + $this->assertNull($o->getData('wfefwe')); + $this->assertEquals(array( + 'parameters' => array(), + 'class' => 'Guzzle\Service\Command\OperationCommand', + 'data' => array('foo' => 'baz', 'bar' => 123, 'test' => false), + 'responseClass' => 'array', + 'responseType' => 'primitive' + ), $o->toArray()); + } + + public function testHasServiceDescription() + { + $s = new ServiceDescription(); + $o = new Operation(array(), $s); + $this->assertSame($s, $o->getServiceDescription()); + } + + /** + * @expectedException Guzzle\Common\Exception\InvalidArgumentException + */ + public function testValidatesResponseType() + { + $o = new Operation(array('responseClass' => 'array', 'responseType' => 'foo')); + } + + public function testInfersResponseType() + { + $o = $this->getOperation(); + $o->setServiceDescription(new ServiceDescription(array('models' => array('Foo' => array())))); + $this->assertEquals('primitive', $o->getResponseType()); + $this->assertEquals('primitive', $o->setResponseClass('boolean')->getResponseType()); + $this->assertEquals('primitive', $o->setResponseClass('array')->getResponseType()); + $this->assertEquals('primitive', $o->setResponseClass('integer')->getResponseType()); + $this->assertEquals('primitive', $o->setResponseClass('string')->getResponseType()); + $this->assertEquals('class', $o->setResponseClass('foo')->getResponseType()); + $this->assertEquals('class', $o->setResponseClass(__CLASS__)->getResponseType()); + $this->assertEquals('model', $o->setResponseClass('Foo')->getResponseType()); + } + + public function testHasResponseType() + { + // infers in the constructor + $o = new Operation(array('responseClass' => 'array')); + $this->assertEquals('primitive', $o->getResponseType()); + // Infers when set + $o = new Operation(); + $this->assertEquals('primitive', $o->getResponseType()); + $this->assertEquals('model', $o->setResponseType('model')->getResponseType()); + } + + public function testHasAdditionalParameters() + { + $o = new Operation(array( + 'additionalParameters' => array( + 'type' => 'string', 'name' => 'binks' + ), + 'parameters' => array( + 'foo' => array('type' => 'integer') + ) + )); + $this->assertEquals('string', $o->getAdditionalParameters()->getType()); + $arr = $o->toArray(); + $this->assertEquals(array( + 'type' => 'string' + ), $arr['additionalParameters']); + } + + /** + * @return Operation + */ + protected function getOperation() + { + return new Operation(array( + 'name' => 'OperationTest', + 'class' => get_class($this), + 'parameters' => array( + 'test' => array('type' => 'object'), + 'bool_1' => array('default' => true, 'type' => 'boolean'), + 'bool_2' => array('default' => false), + 'float' => array('type' => 'numeric'), + 'int' => array('type' => 'integer'), + 'date' => array('type' => 'string'), + 'timestamp' => array('type' => 'string'), + 'string' => array('type' => 'string'), + 'username' => array('type' => 'string', 'required' => true, 'filters' => 'strtolower'), + 'test_function' => array('type' => 'string', 'filters' => __CLASS__ . '::strtoupper') + ), + 'errorResponses' => array( + array('code' => 503, 'reason' => 'InsufficientCapacity', 'class' => 'Guzzle\\Exception\\RuntimeException') + ) + )); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Service/Description/ParameterTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Service/Description/ParameterTest.php new file mode 100644 index 000000000..b9c162aae --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Service/Description/ParameterTest.php @@ -0,0 +1,411 @@ + 'foo', + 'type' => 'bar', + 'required' => true, + 'default' => '123', + 'description' => '456', + 'minLength' => 2, + 'maxLength' => 5, + 'location' => 'body', + 'static' => 'static!', + 'filters' => array('trim', 'json_encode') + ); + + public function testCreatesParamFromArray() + { + $p = new Parameter($this->data); + $this->assertEquals('foo', $p->getName()); + $this->assertEquals('bar', $p->getType()); + $this->assertEquals(true, $p->getRequired()); + $this->assertEquals('123', $p->getDefault()); + $this->assertEquals('456', $p->getDescription()); + $this->assertEquals(2, $p->getMinLength()); + $this->assertEquals(5, $p->getMaxLength()); + $this->assertEquals('body', $p->getLocation()); + $this->assertEquals('static!', $p->getStatic()); + $this->assertEquals(array('trim', 'json_encode'), $p->getFilters()); + } + + public function testCanConvertToArray() + { + $p = new Parameter($this->data); + unset($this->data['name']); + $this->assertEquals($this->data, $p->toArray()); + } + + public function testUsesStatic() + { + $d = $this->data; + $d['default'] = 'booboo'; + $d['static'] = true; + $p = new Parameter($d); + $this->assertEquals('booboo', $p->getValue('bar')); + } + + public function testUsesDefault() + { + $d = $this->data; + $d['default'] = 'foo'; + $d['static'] = null; + $p = new Parameter($d); + $this->assertEquals('foo', $p->getValue(null)); + } + + public function testReturnsYourValue() + { + $d = $this->data; + $d['static'] = null; + $p = new Parameter($d); + $this->assertEquals('foo', $p->getValue('foo')); + } + + public function testZeroValueDoesNotCauseDefaultToBeReturned() + { + $d = $this->data; + $d['default'] = '1'; + $d['static'] = null; + $p = new Parameter($d); + $this->assertEquals('0', $p->getValue('0')); + } + + public function testFiltersValues() + { + $d = $this->data; + $d['static'] = null; + $d['filters'] = 'strtoupper'; + $p = new Parameter($d); + $this->assertEquals('FOO', $p->filter('foo')); + } + + public function testConvertsBooleans() + { + $p = new Parameter(array('type' => 'boolean')); + $this->assertEquals(true, $p->filter('true')); + $this->assertEquals(false, $p->filter('false')); + } + + public function testUsesArrayByDefaultForFilters() + { + $d = $this->data; + $d['filters'] = null; + $p = new Parameter($d); + $this->assertEquals(array(), $p->getFilters()); + } + + public function testAllowsSimpleLocationValue() + { + $p = new Parameter(array('name' => 'myname', 'location' => 'foo', 'sentAs' => 'Hello')); + $this->assertEquals('foo', $p->getLocation()); + $this->assertEquals('Hello', $p->getSentAs()); + } + + public function testParsesTypeValues() + { + $p = new Parameter(array('type' => 'foo')); + $this->assertEquals('foo', $p->getType()); + } + + /** + * @expectedException InvalidArgumentException + * @expectedExceptionMessage A [method] value must be specified for each complex filter + */ + public function testValidatesComplexFilters() + { + $p = new Parameter(array('filters' => array(array('args' => 'foo')))); + } + + public function testCanBuildUpParams() + { + $p = new Parameter(array()); + $p->setName('foo') + ->setDescription('c') + ->setFilters(array('d')) + ->setLocation('e') + ->setSentAs('f') + ->setMaxLength(1) + ->setMinLength(1) + ->setMinimum(2) + ->setMaximum(2) + ->setMinItems(3) + ->setMaxItems(3) + ->setRequired(true) + ->setStatic(true) + ->setDefault('h') + ->setType('i'); + + $p->addFilter('foo'); + + $this->assertEquals('foo', $p->getName()); + $this->assertEquals('h', $p->getDefault()); + $this->assertEquals('c', $p->getDescription()); + $this->assertEquals(array('d', 'foo'), $p->getFilters()); + $this->assertEquals('e', $p->getLocation()); + $this->assertEquals('f', $p->getSentAs()); + $this->assertEquals(1, $p->getMaxLength()); + $this->assertEquals(1, $p->getMinLength()); + $this->assertEquals(2, $p->getMaximum()); + $this->assertEquals(2, $p->getMinimum()); + $this->assertEquals(3, $p->getMaxItems()); + $this->assertEquals(3, $p->getMinItems()); + $this->assertEquals(true, $p->getRequired()); + $this->assertEquals(true, $p->getStatic()); + $this->assertEquals('i', $p->getType()); + } + + public function testAllowsNestedShape() + { + $command = $this->getServiceBuilder()->get('mock')->getCommand('mock_command')->getOperation(); + $param = new Parameter(array( + 'parent' => $command, + 'name' => 'foo', + 'type' => 'object', + 'location' => 'query', + 'properties' => array( + 'foo' => array( + 'type' => 'object', + 'required' => true, + 'properties' => array( + 'baz' => array( + 'name' => 'baz', + 'type' => 'bool', + ) + ) + ), + 'bar' => array( + 'name' => 'bar', + 'default' => '123' + ) + ) + )); + + $this->assertSame($command, $param->getParent()); + $this->assertNotEmpty($param->getProperties()); + $this->assertInstanceOf('Guzzle\Service\Description\Parameter', $param->getProperty('foo')); + $this->assertSame($param, $param->getProperty('foo')->getParent()); + $this->assertSame($param->getProperty('foo'), $param->getProperty('foo')->getProperty('baz')->getParent()); + $this->assertInstanceOf('Guzzle\Service\Description\Parameter', $param->getProperty('bar')); + $this->assertSame($param, $param->getProperty('bar')->getParent()); + + $array = $param->toArray(); + $this->assertInternalType('array', $array['properties']); + $this->assertArrayHasKey('foo', $array['properties']); + $this->assertArrayHasKey('bar', $array['properties']); + } + + public function testAllowsComplexFilters() + { + $that = $this; + $param = new Parameter(array()); + $param->setFilters(array(array('method' => function ($a, $b, $c, $d) use ($that, $param) { + $that->assertEquals('test', $a); + $that->assertEquals('my_value!', $b); + $that->assertEquals('bar', $c); + $that->assertSame($param, $d); + return 'abc' . $b; + }, 'args' => array('test', '@value', 'bar', '@api')))); + $this->assertEquals('abcmy_value!', $param->filter('my_value!')); + } + + public function testCanChangeParentOfNestedParameter() + { + $param1 = new Parameter(array('name' => 'parent')); + $param2 = new Parameter(array('name' => 'child')); + $param2->setParent($param1); + $this->assertSame($param1, $param2->getParent()); + } + + public function testCanRemoveFromNestedStructure() + { + $param1 = new Parameter(array('name' => 'parent')); + $param2 = new Parameter(array('name' => 'child')); + $param1->addProperty($param2); + $this->assertSame($param1, $param2->getParent()); + $this->assertSame($param2, $param1->getProperty('child')); + + // Remove a single child from the structure + $param1->removeProperty('child'); + $this->assertNull($param1->getProperty('child')); + // Remove the entire structure + $param1->addProperty($param2); + $param1->removeProperty('child'); + $this->assertNull($param1->getProperty('child')); + } + + public function testAddsAdditionalProperties() + { + $p = new Parameter(array( + 'type' => 'object', + 'additionalProperties' => array('type' => 'string') + )); + $this->assertInstanceOf('Guzzle\Service\Description\Parameter', $p->getAdditionalProperties()); + $this->assertNull($p->getAdditionalProperties()->getAdditionalProperties()); + $p = new Parameter(array('type' => 'object')); + $this->assertTrue($p->getAdditionalProperties()); + } + + public function testAddsItems() + { + $p = new Parameter(array( + 'type' => 'array', + 'items' => array('type' => 'string') + )); + $this->assertInstanceOf('Guzzle\Service\Description\Parameter', $p->getItems()); + $out = $p->toArray(); + $this->assertEquals('array', $out['type']); + $this->assertInternalType('array', $out['items']); + } + + public function testHasExtraProperties() + { + $p = new Parameter(); + $this->assertEquals(array(), $p->getData()); + $p->setData(array('foo' => 'bar')); + $this->assertEquals('bar', $p->getData('foo')); + $p->setData('baz', 'boo'); + $this->assertEquals(array('foo' => 'bar', 'baz' => 'boo'), $p->getData()); + } + + public function testCanRetrieveKnownPropertiesUsingDataMethod() + { + $p = new Parameter(); + $this->assertEquals(null, $p->getData('foo')); + $p->setName('test'); + $this->assertEquals('test', $p->getData('name')); + } + + public function testHasInstanceOf() + { + $p = new Parameter(); + $this->assertNull($p->getInstanceOf()); + $p->setInstanceOf('Foo'); + $this->assertEquals('Foo', $p->getInstanceOf()); + } + + public function testHasPattern() + { + $p = new Parameter(); + $this->assertNull($p->getPattern()); + $p->setPattern('/[0-9]+/'); + $this->assertEquals('/[0-9]+/', $p->getPattern()); + } + + public function testHasEnum() + { + $p = new Parameter(); + $this->assertNull($p->getEnum()); + $p->setEnum(array('foo', 'bar')); + $this->assertEquals(array('foo', 'bar'), $p->getEnum()); + } + + public function testSerializesItems() + { + $p = new Parameter(array( + 'type' => 'object', + 'additionalProperties' => array('type' => 'string') + )); + $this->assertEquals(array( + 'type' => 'object', + 'additionalProperties' => array('type' => 'string') + ), $p->toArray()); + } + + public function testResolvesRefKeysRecursively() + { + $description = new ServiceDescription(array( + 'models' => array( + 'JarJar' => array('type' => 'string', 'default' => 'Mesa address tha senate!'), + 'Anakin' => array('type' => 'array', 'items' => array('$ref' => 'JarJar')) + ) + )); + $p = new Parameter(array('$ref' => 'Anakin', 'description' => 'added'), $description); + $this->assertEquals(array( + 'type' => 'array', + 'items' => array('type' => 'string', 'default' => 'Mesa address tha senate!'), + 'description' => 'added' + ), $p->toArray()); + } + + public function testResolvesExtendsRecursively() + { + $jarJar = array('type' => 'string', 'default' => 'Mesa address tha senate!', 'description' => 'a'); + $anakin = array('type' => 'array', 'items' => array('extends' => 'JarJar', 'description' => 'b')); + $description = new ServiceDescription(array( + 'models' => array('JarJar' => $jarJar, 'Anakin' => $anakin) + )); + // Description attribute will be updated, and format added + $p = new Parameter(array('extends' => 'Anakin', 'format' => 'date'), $description); + $this->assertEquals(array( + 'type' => 'array', + 'format' => 'date', + 'items' => array( + 'type' => 'string', + 'default' => 'Mesa address tha senate!', + 'description' => 'b' + ) + ), $p->toArray()); + } + + public function testHasKeyMethod() + { + $p = new Parameter(array('name' => 'foo', 'sentAs' => 'bar')); + $this->assertEquals('bar', $p->getWireName()); + $p->setSentAs(null); + $this->assertEquals('foo', $p->getWireName()); + } + + public function testIncludesNameInToArrayWhenItemsAttributeHasName() + { + $p = new Parameter(array( + 'type' => 'array', + 'name' => 'Abc', + 'items' => array( + 'name' => 'Foo', + 'type' => 'object' + ) + )); + $result = $p->toArray(); + $this->assertEquals(array( + 'type' => 'array', + 'items' => array( + 'name' => 'Foo', + 'type' => 'object', + 'additionalProperties' => true + ) + ), $result); + } + + public function dateTimeProvider() + { + $d = 'October 13, 2012 16:15:46 UTC'; + + return array( + array($d, 'date-time', '2012-10-13T16:15:46Z'), + array($d, 'date', '2012-10-13'), + array($d, 'timestamp', strtotime($d)), + array(new \DateTime($d), 'timestamp', strtotime($d)) + ); + } + + /** + * @dataProvider dateTimeProvider + */ + public function testAppliesFormat($d, $format, $result) + { + $p = new Parameter(); + $p->setFormat($format); + $this->assertEquals($format, $p->getFormat()); + $this->assertEquals($result, $p->filter($d)); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Service/Description/SchemaFormatterTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Service/Description/SchemaFormatterTest.php new file mode 100644 index 000000000..eb3619bea --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Service/Description/SchemaFormatterTest.php @@ -0,0 +1,61 @@ +assertEquals($result, SchemaFormatter::format($format, $value)); + } + + /** + * @expectedException \Guzzle\Common\Exception\InvalidArgumentException + */ + public function testValidatesDateTimeInput() + { + SchemaFormatter::format('date-time', false); + } + + public function testEnsuresTimestampsAreIntegers() + { + $t = time(); + $result = SchemaFormatter::format('timestamp', $t); + $this->assertSame($t, $result); + $this->assertInternalType('int', $result); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Service/Description/SchemaValidatorTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Service/Description/SchemaValidatorTest.php new file mode 100644 index 000000000..4d6cc8728 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Service/Description/SchemaValidatorTest.php @@ -0,0 +1,326 @@ +validator = new SchemaValidator(); + } + + public function testValidatesArrayListsAreNumericallyIndexed() + { + $value = array(array(1)); + $this->assertFalse($this->validator->validate($this->getComplexParam(), $value)); + $this->assertEquals( + array('[Foo][0] must be an array of properties. Got a numerically indexed array.'), + $this->validator->getErrors() + ); + } + + public function testValidatesArrayListsContainProperItems() + { + $value = array(true); + $this->assertFalse($this->validator->validate($this->getComplexParam(), $value)); + $this->assertEquals( + array('[Foo][0] must be of type object'), + $this->validator->getErrors() + ); + } + + public function testAddsDefaultValuesInLists() + { + $value = array(array()); + $this->assertTrue($this->validator->validate($this->getComplexParam(), $value)); + $this->assertEquals(array(array('Bar' => true)), $value); + } + + public function testMergesDefaultValuesInLists() + { + $value = array( + array('Baz' => 'hello!'), + array('Bar' => false) + ); + $this->assertTrue($this->validator->validate($this->getComplexParam(), $value)); + $this->assertEquals(array( + array( + 'Baz' => 'hello!', + 'Bar' => true + ), + array('Bar' => false) + ), $value); + } + + public function testCorrectlyConvertsParametersToArrayWhenArraysArePresent() + { + $param = $this->getComplexParam(); + $result = $param->toArray(); + $this->assertInternalType('array', $result['items']); + $this->assertEquals('array', $result['type']); + $this->assertInstanceOf('Guzzle\Service\Description\Parameter', $param->getItems()); + } + + public function testAllowsInstanceOf() + { + $p = new Parameter(array( + 'name' => 'foo', + 'type' => 'object', + 'instanceOf' => get_class($this) + )); + $this->assertTrue($this->validator->validate($p, $this)); + $this->assertFalse($this->validator->validate($p, $p)); + $this->assertEquals(array('[foo] must be an instance of ' . __CLASS__), $this->validator->getErrors()); + } + + public function testEnforcesInstanceOfOnlyWhenObject() + { + $p = new Parameter(array( + 'name' => 'foo', + 'type' => array('object', 'string'), + 'instanceOf' => get_class($this) + )); + $this->assertTrue($this->validator->validate($p, $this)); + $s = 'test'; + $this->assertTrue($this->validator->validate($p, $s)); + } + + public function testConvertsObjectsToArraysWhenToArrayInterface() + { + $o = $this->getMockBuilder('Guzzle\Common\ToArrayInterface') + ->setMethods(array('toArray')) + ->getMockForAbstractClass(); + $o->expects($this->once()) + ->method('toArray') + ->will($this->returnValue(array( + 'foo' => 'bar' + ))); + $p = new Parameter(array( + 'name' => 'test', + 'type' => 'object', + 'properties' => array( + 'foo' => array('required' => 'true') + ) + )); + $this->assertTrue($this->validator->validate($p, $o)); + } + + public function testMergesValidationErrorsInPropertiesWithParent() + { + $p = new Parameter(array( + 'name' => 'foo', + 'type' => 'object', + 'properties' => array( + 'bar' => array('type' => 'string', 'required' => true, 'description' => 'This is what it does'), + 'test' => array('type' => 'string', 'minLength' => 2, 'maxLength' => 5), + 'test2' => array('type' => 'string', 'minLength' => 2, 'maxLength' => 2), + 'test3' => array('type' => 'integer', 'minimum' => 100), + 'test4' => array('type' => 'integer', 'maximum' => 10), + 'test5' => array('type' => 'array', 'maxItems' => 2), + 'test6' => array('type' => 'string', 'enum' => array('a', 'bc')), + 'test7' => array('type' => 'string', 'pattern' => '/[0-9]+/'), + 'test8' => array('type' => 'number'), + 'baz' => array( + 'type' => 'array', + 'minItems' => 2, + 'required' => true, + "items" => array("type" => "string") + ) + ) + )); + + $value = array( + 'test' => 'a', + 'test2' => 'abc', + 'baz' => array(false), + 'test3' => 10, + 'test4' => 100, + 'test5' => array(1, 3, 4), + 'test6' => 'Foo', + 'test7' => 'abc', + 'test8' => 'abc' + ); + + $this->assertFalse($this->validator->validate($p, $value)); + $this->assertEquals(array ( + '[foo][bar] is a required string: This is what it does', + '[foo][baz] must contain 2 or more elements', + '[foo][baz][0] must be of type string', + '[foo][test2] length must be less than or equal to 2', + '[foo][test3] must be greater than or equal to 100', + '[foo][test4] must be less than or equal to 10', + '[foo][test5] must contain 2 or fewer elements', + '[foo][test6] must be one of "a" or "bc"', + '[foo][test7] must match the following regular expression: /[0-9]+/', + '[foo][test8] must be of type number', + '[foo][test] length must be greater than or equal to 2', + ), $this->validator->getErrors()); + } + + public function testHandlesNullValuesInArraysWithDefaults() + { + $p = new Parameter(array( + 'name' => 'foo', + 'type' => 'object', + 'required' => true, + 'properties' => array( + 'bar' => array( + 'type' => 'object', + 'required' => true, + 'properties' => array( + 'foo' => array('default' => 'hi') + ) + ) + ) + )); + $value = array(); + $this->assertTrue($this->validator->validate($p, $value)); + $this->assertEquals(array('bar' => array('foo' => 'hi')), $value); + } + + public function testFailsWhenNullValuesInArraysWithNoDefaults() + { + $p = new Parameter(array( + 'name' => 'foo', + 'type' => 'object', + 'required' => true, + 'properties' => array( + 'bar' => array( + 'type' => 'object', + 'required' => true, + 'properties' => array('foo' => array('type' => 'string')) + ) + ) + )); + $value = array(); + $this->assertFalse($this->validator->validate($p, $value)); + $this->assertEquals(array('[foo][bar] is a required object'), $this->validator->getErrors()); + } + + public function testChecksTypes() + { + $p = new SchemaValidator(); + $r = new \ReflectionMethod($p, 'determineType'); + $r->setAccessible(true); + $this->assertEquals('any', $r->invoke($p, 'any', 'hello')); + $this->assertEquals(false, $r->invoke($p, 'foo', 'foo')); + $this->assertEquals('string', $r->invoke($p, 'string', 'hello')); + $this->assertEquals(false, $r->invoke($p, 'string', false)); + $this->assertEquals('integer', $r->invoke($p, 'integer', 1)); + $this->assertEquals(false, $r->invoke($p, 'integer', 'abc')); + $this->assertEquals('numeric', $r->invoke($p, 'numeric', 1)); + $this->assertEquals('numeric', $r->invoke($p, 'numeric', '1')); + $this->assertEquals('number', $r->invoke($p, 'number', 1)); + $this->assertEquals('number', $r->invoke($p, 'number', '1')); + $this->assertEquals(false, $r->invoke($p, 'numeric', 'a')); + $this->assertEquals('boolean', $r->invoke($p, 'boolean', true)); + $this->assertEquals('boolean', $r->invoke($p, 'boolean', false)); + $this->assertEquals(false, $r->invoke($p, 'boolean', 'false')); + $this->assertEquals('null', $r->invoke($p, 'null', null)); + $this->assertEquals(false, $r->invoke($p, 'null', 'abc')); + $this->assertEquals('array', $r->invoke($p, 'array', array())); + $this->assertEquals(false, $r->invoke($p, 'array', 'foo')); + } + + public function testValidatesFalseAdditionalProperties() + { + $param = new Parameter(array( + 'name' => 'foo', + 'type' => 'object', + 'properties' => array('bar' => array('type' => 'string')), + 'additionalProperties' => false + )); + $value = array('test' => '123'); + $this->assertFalse($this->validator->validate($param, $value)); + $this->assertEquals(array('[foo][test] is not an allowed property'), $this->validator->getErrors()); + $value = array('bar' => '123'); + $this->assertTrue($this->validator->validate($param, $value)); + } + + public function testAllowsUndefinedAdditionalProperties() + { + $param = new Parameter(array( + 'name' => 'foo', + 'type' => 'object', + 'properties' => array('bar' => array('type' => 'string')) + )); + $value = array('test' => '123'); + $this->assertTrue($this->validator->validate($param, $value)); + } + + public function testValidatesAdditionalProperties() + { + $param = new Parameter(array( + 'name' => 'foo', + 'type' => 'object', + 'properties' => array('bar' => array('type' => 'string')), + 'additionalProperties' => array('type' => 'integer') + )); + $value = array('test' => 'foo'); + $this->assertFalse($this->validator->validate($param, $value)); + $this->assertEquals(array('[foo][test] must be of type integer'), $this->validator->getErrors()); + } + + public function testValidatesAdditionalPropertiesThatArrayArrays() + { + $param = new Parameter(array( + 'name' => 'foo', + 'type' => 'object', + 'additionalProperties' => array( + 'type' => 'array', + 'items' => array('type' => 'string') + ) + )); + $value = array('test' => array(true)); + $this->assertFalse($this->validator->validate($param, $value)); + $this->assertEquals(array('[foo][test][0] must be of type string'), $this->validator->getErrors()); + } + + public function testIntegersCastToStringWhenTypeMismatch() + { + $param = new Parameter(array('name' => 'test', 'type' => 'string')); + $value = 12; + $this->assertTrue($this->validator->validate($param, $value)); + $this->assertEquals('12', $value); + } + + public function testRequiredMessageIncludesType() + { + $param = new Parameter(array('name' => 'test', 'type' => array('string', 'boolean'), 'required' => true)); + $value = null; + $this->assertFalse($this->validator->validate($param, $value)); + $this->assertEquals(array('[test] is a required string or boolean'), $this->validator->getErrors()); + } + + protected function getComplexParam() + { + return new Parameter(array( + 'name' => 'Foo', + 'type' => 'array', + 'required' => true, + 'min' => 1, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'Baz' => array( + 'type' => 'string', + ), + 'Bar' => array( + 'required' => true, + 'type' => 'boolean', + 'default' => true + ) + ) + ) + )); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Service/Description/ServiceDescriptionLoaderTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Service/Description/ServiceDescriptionLoaderTest.php new file mode 100644 index 000000000..bbfd1d6df --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Service/Description/ServiceDescriptionLoaderTest.php @@ -0,0 +1,177 @@ + true, + 'baz' => array('bar'), + 'apiVersion' => '123', + 'operations' => array() + )); + + $this->assertEquals(true, $d->getData('foo')); + $this->assertEquals(array('bar'), $d->getData('baz')); + $this->assertEquals('123', $d->getApiVersion()); + } + + public function testAllowsDeepNestedInheritance() + { + $d = ServiceDescription::factory(array( + 'operations' => array( + 'abstract' => array( + 'httpMethod' => 'HEAD', + 'parameters' => array( + 'test' => array('type' => 'string', 'required' => true) + ) + ), + 'abstract2' => array('uri' => '/test', 'extends' => 'abstract'), + 'concrete' => array('extends' => 'abstract2'), + 'override' => array('extends' => 'abstract', 'httpMethod' => 'PUT'), + 'override2' => array('extends' => 'override', 'httpMethod' => 'POST', 'uri' => '/') + ) + )); + + $c = $d->getOperation('concrete'); + $this->assertEquals('/test', $c->getUri()); + $this->assertEquals('HEAD', $c->getHttpMethod()); + $params = $c->getParams(); + $param = $params['test']; + $this->assertEquals('string', $param->getType()); + $this->assertTrue($param->getRequired()); + + // Ensure that merging HTTP method does not make an array + $this->assertEquals('PUT', $d->getOperation('override')->getHttpMethod()); + $this->assertEquals('POST', $d->getOperation('override2')->getHttpMethod()); + $this->assertEquals('/', $d->getOperation('override2')->getUri()); + } + + /** + * @expectedException RuntimeException + */ + public function testThrowsExceptionWhenExtendingMissingCommand() + { + ServiceDescription::factory(array( + 'operations' => array( + 'concrete' => array( + 'extends' => 'missing' + ) + ) + )); + } + + public function testAllowsMultipleInheritance() + { + $description = ServiceDescription::factory(array( + 'operations' => array( + 'a' => array( + 'httpMethod' => 'GET', + 'parameters' => array( + 'a1' => array( + 'default' => 'foo', + 'required' => true, + 'prepend' => 'hi' + ) + ) + ), + 'b' => array( + 'extends' => 'a', + 'parameters' => array( + 'b2' => array() + ) + ), + 'c' => array( + 'parameters' => array( + 'a1' => array( + 'default' => 'bar', + 'required' => true, + 'description' => 'test' + ), + 'c3' => array() + ) + ), + 'd' => array( + 'httpMethod' => 'DELETE', + 'extends' => array('b', 'c'), + 'parameters' => array( + 'test' => array() + ) + ) + ) + )); + + $command = $description->getOperation('d'); + $this->assertEquals('DELETE', $command->getHttpMethod()); + $this->assertContains('a1', $command->getParamNames()); + $this->assertContains('b2', $command->getParamNames()); + $this->assertContains('c3', $command->getParamNames()); + $this->assertContains('test', $command->getParamNames()); + + $this->assertTrue($command->getParam('a1')->getRequired()); + $this->assertEquals('bar', $command->getParam('a1')->getDefault()); + $this->assertEquals('test', $command->getParam('a1')->getDescription()); + } + + public function testAddsOtherFields() + { + $description = ServiceDescription::factory(array( + 'operations' => array(), + 'description' => 'Foo', + 'apiVersion' => 'bar' + )); + $this->assertEquals('Foo', $description->getDescription()); + $this->assertEquals('bar', $description->getApiVersion()); + } + + public function testCanLoadNestedExtends() + { + $description = ServiceDescription::factory(array( + 'operations' => array( + 'root' => array( + 'class' => 'foo' + ), + 'foo' => array( + 'extends' => 'root', + 'parameters' => array( + 'baz' => array('type' => 'string') + ) + ), + 'foo_2' => array( + 'extends' => 'foo', + 'parameters' => array( + 'bar' => array('type' => 'string') + ) + ), + 'foo_3' => array( + 'class' => 'bar', + 'parameters' => array( + 'bar2' => array('type' => 'string') + ) + ), + 'foo_4' => array( + 'extends' => array('foo_2', 'foo_3'), + 'parameters' => array( + 'bar3' => array('type' => 'string') + ) + ) + ) + )); + + $this->assertTrue($description->hasOperation('foo_4')); + $foo4 = $description->getOperation('foo_4'); + $this->assertTrue($foo4->hasParam('baz')); + $this->assertTrue($foo4->hasParam('bar')); + $this->assertTrue($foo4->hasParam('bar2')); + $this->assertTrue($foo4->hasParam('bar3')); + $this->assertEquals('bar', $foo4->getClass()); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Service/Description/ServiceDescriptionTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Service/Description/ServiceDescriptionTest.php new file mode 100644 index 000000000..ff2545235 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Service/Description/ServiceDescriptionTest.php @@ -0,0 +1,240 @@ +serviceData = array( + 'test_command' => new Operation(array( + 'name' => 'test_command', + 'description' => 'documentationForCommand', + 'httpMethod' => 'DELETE', + 'class' => 'Guzzle\\Tests\\Service\\Mock\\Command\\MockCommand', + 'parameters' => array( + 'bucket' => array('required' => true), + 'key' => array('required' => true) + ) + )) + ); + } + + /** + * @covers Guzzle\Service\Description\ServiceDescription::factory + * @covers Guzzle\Service\Description\ServiceDescriptionLoader::build + */ + public function testFactoryDelegatesToConcreteFactories() + { + $jsonFile = __DIR__ . '/../../TestData/test_service.json'; + $this->assertInstanceOf('Guzzle\Service\Description\ServiceDescription', ServiceDescription::factory($jsonFile)); + } + + public function testConstructor() + { + $service = new ServiceDescription(array('operations' => $this->serviceData)); + $this->assertEquals(1, count($service->getOperations())); + $this->assertFalse($service->hasOperation('foobar')); + $this->assertTrue($service->hasOperation('test_command')); + } + + public function testIsSerializable() + { + $service = new ServiceDescription(array('operations' => $this->serviceData)); + $data = serialize($service); + $d2 = unserialize($data); + $this->assertEquals(serialize($service), serialize($d2)); + } + + public function testSerializesParameters() + { + $service = new ServiceDescription(array( + 'operations' => array( + 'foo' => new Operation(array('parameters' => array('foo' => array('type' => 'string')))) + ) + )); + $serialized = serialize($service); + $this->assertContains('"parameters":{"foo":', $serialized); + $service = unserialize($serialized); + $this->assertTrue($service->getOperation('foo')->hasParam('foo')); + } + + public function testAllowsForJsonBasedArrayParamsFunctionalTest() + { + $description = new ServiceDescription(array( + 'operations' => array( + 'test' => new Operation(array( + 'httpMethod' => 'PUT', + 'parameters' => array( + 'data' => array( + 'required' => true, + 'filters' => 'json_encode', + 'location' => 'body' + ) + ) + )) + ) + )); + $client = new Client(); + $client->setDescription($description); + $command = $client->getCommand('test', array( + 'data' => array( + 'foo' => 'bar' + ) + )); + + $request = $command->prepare(); + $this->assertEquals('{"foo":"bar"}', (string) $request->getBody()); + } + + public function testContainsModels() + { + $d = new ServiceDescription(array( + 'operations' => array('foo' => array()), + 'models' => array( + 'Tag' => array('type' => 'object'), + 'Person' => array('type' => 'object') + ) + )); + $this->assertTrue($d->hasModel('Tag')); + $this->assertTrue($d->hasModel('Person')); + $this->assertFalse($d->hasModel('Foo')); + $this->assertInstanceOf('Guzzle\Service\Description\Parameter', $d->getModel('Tag')); + $this->assertNull($d->getModel('Foo')); + $this->assertContains('"models":{', serialize($d)); + $this->assertEquals(array('Tag', 'Person'), array_keys($d->getModels())); + } + + public function testCanAddModels() + { + $d = new ServiceDescription(array()); + $this->assertFalse($d->hasModel('Foo')); + $d->addModel(new Parameter(array('name' => 'Foo'))); + $this->assertTrue($d->hasModel('Foo')); + } + + public function testHasAttributes() + { + $d = new ServiceDescription(array( + 'operations' => array(), + 'name' => 'Name', + 'description' => 'Description', + 'apiVersion' => '1.24' + )); + + $this->assertEquals('Name', $d->getName()); + $this->assertEquals('Description', $d->getDescription()); + $this->assertEquals('1.24', $d->getApiVersion()); + + $s = serialize($d); + $this->assertContains('"name":"Name"', $s); + $this->assertContains('"description":"Description"', $s); + $this->assertContains('"apiVersion":"1.24"', $s); + + $d = unserialize($s); + $this->assertEquals('Name', $d->getName()); + $this->assertEquals('Description', $d->getDescription()); + $this->assertEquals('1.24', $d->getApiVersion()); + } + + public function testPersistsCustomAttributes() + { + $data = array( + 'operations' => array('foo' => array('class' => 'foo', 'parameters' => array())), + 'name' => 'Name', + 'description' => 'Test', + 'apiVersion' => '1.24', + 'auth' => 'foo', + 'keyParam' => 'bar' + ); + $d = new ServiceDescription($data); + $d->setData('hello', 'baz'); + $this->assertEquals('foo', $d->getData('auth')); + $this->assertEquals('baz', $d->getData('hello')); + $this->assertEquals('bar', $d->getData('keyParam')); + // responseClass and responseType are added by default + $data['operations']['foo']['responseClass'] = 'array'; + $data['operations']['foo']['responseType'] = 'primitive'; + $this->assertEquals($data + array('hello' => 'baz'), json_decode($d->serialize(), true)); + } + + public function testHasToArray() + { + $data = array( + 'operations' => array(), + 'name' => 'Name', + 'description' => 'Test' + ); + $d = new ServiceDescription($data); + $arr = $d->toArray(); + $this->assertEquals('Name', $arr['name']); + $this->assertEquals('Test', $arr['description']); + } + + public function testReturnsNullWhenRetrievingMissingOperation() + { + $s = new ServiceDescription(array()); + $this->assertNull($s->getOperation('foo')); + } + + public function testCanAddOperations() + { + $s = new ServiceDescription(array()); + $this->assertFalse($s->hasOperation('Foo')); + $s->addOperation(new Operation(array('name' => 'Foo'))); + $this->assertTrue($s->hasOperation('Foo')); + } + + /** + * @expectedException Guzzle\Common\Exception\InvalidArgumentException + */ + public function testValidatesOperationTypes() + { + $s = new ServiceDescription(array( + 'operations' => array('foo' => new \stdClass()) + )); + } + + public function testHasBaseUrl() + { + $description = new ServiceDescription(array('baseUrl' => 'http://foo.com')); + $this->assertEquals('http://foo.com', $description->getBaseUrl()); + $description->setBaseUrl('http://foobar.com'); + $this->assertEquals('http://foobar.com', $description->getBaseUrl()); + } + + public function testCanUseBasePath() + { + $description = new ServiceDescription(array('basePath' => 'http://foo.com')); + $this->assertEquals('http://foo.com', $description->getBaseUrl()); + } + + public function testModelsHaveNames() + { + $desc = array( + 'models' => array( + 'date' => array('type' => 'string'), + 'user'=> array( + 'type' => 'object', + 'properties' => array( + 'dob' => array('$ref' => 'date') + ) + ) + ) + ); + + $s = ServiceDescription::factory($desc); + $this->assertEquals('date', $s->getModel('date')->getName()); + $this->assertEquals('dob', $s->getModel('user')->getProperty('dob')->getName()); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Service/Exception/CommandTransferExceptionTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Service/Exception/CommandTransferExceptionTest.php new file mode 100644 index 000000000..be0d4ac8b --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Service/Exception/CommandTransferExceptionTest.php @@ -0,0 +1,66 @@ +addSuccessfulCommand($c1)->addFailedCommand($c2); + $this->assertSame(array($c1), $e->getSuccessfulCommands()); + $this->assertSame(array($c2), $e->getFailedCommands()); + $this->assertSame(array($c1, $c2), $e->getAllCommands()); + } + + public function testConvertsMultiExceptionIntoCommandTransfer() + { + $r1 = new Request('GET', 'http://foo.com'); + $r2 = new Request('GET', 'http://foobaz.com'); + $e = new MultiTransferException('Test', 123); + $e->addSuccessfulRequest($r1)->addFailedRequest($r2); + $ce = CommandTransferException::fromMultiTransferException($e); + + $this->assertInstanceOf('Guzzle\Service\Exception\CommandTransferException', $ce); + $this->assertEquals('Test', $ce->getMessage()); + $this->assertEquals(123, $ce->getCode()); + $this->assertSame(array($r1), $ce->getSuccessfulRequests()); + $this->assertSame(array($r2), $ce->getFailedRequests()); + } + + public function testCanRetrieveExceptionForCommand() + { + $r1 = new Request('GET', 'http://foo.com'); + $e1 = new \Exception('foo'); + $c1 = $this->getMockBuilder('Guzzle\Tests\Service\Mock\Command\MockCommand') + ->setMethods(array('getRequest')) + ->getMock(); + $c1->expects($this->once())->method('getRequest')->will($this->returnValue($r1)); + + $e = new MultiTransferException('Test', 123); + $e->addFailedRequestWithException($r1, $e1); + $ce = CommandTransferException::fromMultiTransferException($e); + $ce->addFailedCommand($c1); + + $this->assertSame($e1, $ce->getExceptionForFailedCommand($c1)); + } + + public function testAddsNonRequestExceptions() + { + $e = new MultiTransferException(); + $e->add(new \Exception('bar')); + $e->addFailedRequestWithException(new Request('GET', 'http://www.foo.com'), new \Exception('foo')); + $ce = CommandTransferException::fromMultiTransferException($e); + $this->assertEquals(2, count($ce)); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Service/Exception/InconsistentClientTransferExceptionTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Service/Exception/InconsistentClientTransferExceptionTest.php new file mode 100644 index 000000000..6455295ad --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Service/Exception/InconsistentClientTransferExceptionTest.php @@ -0,0 +1,15 @@ +assertEquals($items, $e->getCommands()); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Service/Exception/ValidationExceptionTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Service/Exception/ValidationExceptionTest.php new file mode 100644 index 000000000..ef789d8a9 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Service/Exception/ValidationExceptionTest.php @@ -0,0 +1,17 @@ +setErrors($errors); + $this->assertEquals($errors, $e->getErrors()); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Service/Mock/Command/IterableCommand.php b/guzzle/guzzle/tests/Guzzle/Tests/Service/Mock/Command/IterableCommand.php new file mode 100644 index 000000000..4ab423e85 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Service/Mock/Command/IterableCommand.php @@ -0,0 +1,31 @@ + 'iterable_command', + 'parameters' => array( + 'page_size' => array('type' => 'integer'), + 'next_token' => array('type' => 'string') + ) + )); + } + + protected function build() + { + $this->request = $this->client->createRequest('GET'); + + // Add the next token and page size query string values + $this->request->getQuery()->set('next_token', $this->get('next_token')); + + if ($this->get('page_size')) { + $this->request->getQuery()->set('page_size', $this->get('page_size')); + } + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Service/Mock/Command/MockCommand.php b/guzzle/guzzle/tests/Guzzle/Tests/Service/Mock/Command/MockCommand.php new file mode 100644 index 000000000..831a7e795 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Service/Mock/Command/MockCommand.php @@ -0,0 +1,32 @@ + get_called_class() == __CLASS__ ? 'mock_command' : 'sub.sub', + 'httpMethod' => 'POST', + 'parameters' => array( + 'test' => array( + 'default' => 123, + 'required' => true, + 'doc' => 'Test argument' + ), + '_internal' => array( + 'default' => 'abc' + ), + 'foo' => array('filters' => array('strtoupper')) + ) + )); + } + + protected function build() + { + $this->request = $this->client->createRequest(); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Service/Mock/Command/OtherCommand.php b/guzzle/guzzle/tests/Guzzle/Tests/Service/Mock/Command/OtherCommand.php new file mode 100644 index 000000000..72ae1f6a7 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Service/Mock/Command/OtherCommand.php @@ -0,0 +1,30 @@ + 'other_command', + 'parameters' => array( + 'test' => array( + 'default' => '123', + 'required' => true, + 'doc' => 'Test argument' + ), + 'other' => array(), + 'arg' => array('type' => 'string'), + 'static' => array('static' => true, 'default' => 'this is static') + ) + )); + } + + protected function build() + { + $this->request = $this->client->getRequest('HEAD'); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Service/Mock/Command/Sub/Sub.php b/guzzle/guzzle/tests/Guzzle/Tests/Service/Mock/Command/Sub/Sub.php new file mode 100644 index 000000000..d348480cf --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Service/Mock/Command/Sub/Sub.php @@ -0,0 +1,7 @@ + '{scheme}://127.0.0.1:8124/{api_version}/{subdomain}', + 'scheme' => 'http', + 'api_version' => 'v1' + ), array('username', 'password', 'subdomain')); + + return new self($config->get('base_url'), $config); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Service/Mock/Model/MockCommandIterator.php b/guzzle/guzzle/tests/Guzzle/Tests/Service/Mock/Model/MockCommandIterator.php new file mode 100644 index 000000000..8faf4123a --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Service/Mock/Model/MockCommandIterator.php @@ -0,0 +1,42 @@ +nextToken) { + $this->command->set('next_token', $this->nextToken); + } + + $this->command->set('page_size', (int) $this->calculatePageSize()); + $this->command->execute(); + + $data = json_decode($this->command->getResponse()->getBody(true), true); + + $this->nextToken = $data['next_token']; + + return $data['resources']; + } + + public function next() + { + $this->calledNext++; + parent::next(); + } + + public function getResources() + { + return $this->resources; + } + + public function getIteratedCount() + { + return $this->iteratedCount; + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Service/Resource/CompositeResourceIteratorFactoryTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Service/Resource/CompositeResourceIteratorFactoryTest.php new file mode 100644 index 000000000..41c207372 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Service/Resource/CompositeResourceIteratorFactoryTest.php @@ -0,0 +1,37 @@ +assertFalse($factory->canBuild($cmd)); + $factory->build($cmd); + } + + public function testBuildsResourceIterators() + { + $f1 = new ResourceIteratorClassFactory('Guzzle\Tests\Service\Mock\Model'); + $factory = new CompositeResourceIteratorFactory(array()); + $factory->addFactory($f1); + $command = new MockCommand(); + $iterator = $factory->build($command, array('client.namespace' => 'Guzzle\Tests\Service\Mock')); + $this->assertInstanceOf('Guzzle\Tests\Service\Mock\Model\MockCommandIterator', $iterator); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Service/Resource/MapResourceIteratorFactoryTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Service/Resource/MapResourceIteratorFactoryTest.php new file mode 100644 index 000000000..d166e9261 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Service/Resource/MapResourceIteratorFactoryTest.php @@ -0,0 +1,40 @@ +build(new MockCommand()); + } + + public function testBuildsResourceIterators() + { + $factory = new MapResourceIteratorFactory(array( + 'mock_command' => 'Guzzle\Tests\Service\Mock\Model\MockCommandIterator' + )); + $iterator = $factory->build(new MockCommand()); + $this->assertInstanceOf('Guzzle\Tests\Service\Mock\Model\MockCommandIterator', $iterator); + } + + public function testUsesWildcardMappings() + { + $factory = new MapResourceIteratorFactory(array( + '*' => 'Guzzle\Tests\Service\Mock\Model\MockCommandIterator' + )); + $iterator = $factory->build(new MockCommand()); + $this->assertInstanceOf('Guzzle\Tests\Service\Mock\Model\MockCommandIterator', $iterator); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Service/Resource/ModelTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Service/Resource/ModelTest.php new file mode 100644 index 000000000..7214133e5 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Service/Resource/ModelTest.php @@ -0,0 +1,65 @@ + 'object')); + $model = new Model(array('foo' => 'bar'), $param); + $this->assertSame($param, $model->getStructure()); + $this->assertEquals('bar', $model->get('foo')); + $this->assertEquals('bar', $model['foo']); + } + + public function testCanBeUsedWithoutStructure() + { + $model = new Model(array( + 'Foo' => 'baz', + 'Bar' => array( + 'Boo' => 'Bam' + ) + )); + $transform = function ($key, $value) { + return ($value && is_array($value)) ? new Collection($value) : $value; + }; + $model = $model->map($transform); + $this->assertInstanceOf('Guzzle\Common\Collection', $model->getPath('Bar')); + } + + public function testAllowsFiltering() + { + $model = new Model(array( + 'Foo' => 'baz', + 'Bar' => 'a' + )); + $model = $model->filter(function ($i, $v) { + return $v[0] == 'a'; + }); + $this->assertEquals(array('Bar' => 'a'), $model->toArray()); + } + + public function testDoesNotIncludeEmptyStructureInString() + { + $model = new Model(array('Foo' => 'baz')); + $str = (string) $model; + $this->assertContains('Debug output of model', $str); + $this->assertNotContains('Model structure', $str); + } + + public function testDoesIncludeModelStructureInString() + { + $model = new Model(array('Foo' => 'baz'), new Parameter(array('name' => 'Foo'))); + $str = (string) $model; + $this->assertContains('Debug output of Foo model', $str); + $this->assertContains('Model structure', $str); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Service/Resource/ResourceIteratorClassFactoryTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Service/Resource/ResourceIteratorClassFactoryTest.php new file mode 100644 index 000000000..7b061b537 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Service/Resource/ResourceIteratorClassFactoryTest.php @@ -0,0 +1,41 @@ +registerNamespace('Baz'); + $command = new MockCommand(); + $factory->build($command); + } + + public function testBuildsResourceIterators() + { + $factory = new ResourceIteratorClassFactory('Guzzle\Tests\Service\Mock\Model'); + $command = new MockCommand(); + $iterator = $factory->build($command, array('client.namespace' => 'Guzzle\Tests\Service\Mock')); + $this->assertInstanceOf('Guzzle\Tests\Service\Mock\Model\MockCommandIterator', $iterator); + } + + public function testChecksIfCanBuild() + { + $factory = new ResourceIteratorClassFactory('Guzzle\Tests\Service'); + $this->assertFalse($factory->canBuild(new MockCommand())); + $factory = new ResourceIteratorClassFactory('Guzzle\Tests\Service\Mock\Model'); + $this->assertTrue($factory->canBuild(new MockCommand())); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Service/Resource/ResourceIteratorTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Service/Resource/ResourceIteratorTest.php new file mode 100644 index 000000000..2cd03187d --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Service/Resource/ResourceIteratorTest.php @@ -0,0 +1,185 @@ +assertInternalType('array', ResourceIterator::getAllEvents()); + } + + public function testConstructorConfiguresDefaults() + { + $ri = $this->getMockForAbstractClass('Guzzle\\Service\\Resource\\ResourceIterator', array( + $this->getServiceBuilder()->get('mock')->getCommand('iterable_command'), + array( + 'limit' => 10, + 'page_size' => 3 + ) + ), 'MockIterator'); + + $this->assertEquals(false, $ri->getNextToken()); + $this->assertEquals(false, $ri->current()); + } + + public function testSendsRequestsForNextSetOfResources() + { + // Queue up an array of responses for iterating + $this->getServer()->flush(); + $this->getServer()->enqueue(array( + "HTTP/1.1 200 OK\r\nContent-Length: 52\r\n\r\n{ \"next_token\": \"g\", \"resources\": [\"d\", \"e\", \"f\"] }", + "HTTP/1.1 200 OK\r\nContent-Length: 52\r\n\r\n{ \"next_token\": \"j\", \"resources\": [\"g\", \"h\", \"i\"] }", + "HTTP/1.1 200 OK\r\nContent-Length: 41\r\n\r\n{ \"next_token\": \"\", \"resources\": [\"j\"] }" + )); + + // Create a new resource iterator using the IterableCommand mock + $ri = new MockCommandIterator($this->getServiceBuilder()->get('mock')->getCommand('iterable_command'), array( + 'page_size' => 3 + )); + + // Ensure that no requests have been sent yet + $this->assertEquals(0, count($this->getServer()->getReceivedRequests(false))); + + //$this->assertEquals(array('d', 'e', 'f', 'g', 'h', 'i', 'j'), $ri->toArray()); + $ri->toArray(); + $requests = $this->getServer()->getReceivedRequests(true); + $this->assertEquals(3, count($requests)); + + $this->assertEquals(3, $requests[0]->getQuery()->get('page_size')); + $this->assertEquals(3, $requests[1]->getQuery()->get('page_size')); + $this->assertEquals(3, $requests[2]->getQuery()->get('page_size')); + + // Reset and resend + $this->getServer()->flush(); + $this->getServer()->enqueue(array( + "HTTP/1.1 200 OK\r\nContent-Length: 52\r\n\r\n{ \"next_token\": \"g\", \"resources\": [\"d\", \"e\", \"f\"] }", + "HTTP/1.1 200 OK\r\nContent-Length: 52\r\n\r\n{ \"next_token\": \"j\", \"resources\": [\"g\", \"h\", \"i\"] }", + "HTTP/1.1 200 OK\r\nContent-Length: 41\r\n\r\n{ \"next_token\": \"\", \"resources\": [\"j\"] }", + )); + + $d = array(); + reset($ri); + foreach ($ri as $data) { + $d[] = $data; + } + $this->assertEquals(array('d', 'e', 'f', 'g', 'h', 'i', 'j'), $d); + } + + public function testCalculatesPageSize() + { + $this->getServer()->flush(); + $this->getServer()->enqueue(array( + "HTTP/1.1 200 OK\r\nContent-Length: 52\r\n\r\n{ \"next_token\": \"g\", \"resources\": [\"d\", \"e\", \"f\"] }", + "HTTP/1.1 200 OK\r\nContent-Length: 52\r\n\r\n{ \"next_token\": \"j\", \"resources\": [\"g\", \"h\", \"i\"] }", + "HTTP/1.1 200 OK\r\nContent-Length: 52\r\n\r\n{ \"next_token\": \"j\", \"resources\": [\"j\", \"k\"] }" + )); + + $ri = new MockCommandIterator($this->getServiceBuilder()->get('mock')->getCommand('iterable_command'), array( + 'page_size' => 3, + 'limit' => 7 + )); + + $this->assertEquals(array('d', 'e', 'f', 'g', 'h', 'i', 'j'), $ri->toArray()); + $requests = $this->getServer()->getReceivedRequests(true); + $this->assertEquals(3, count($requests)); + $this->assertEquals(3, $requests[0]->getQuery()->get('page_size')); + $this->assertEquals(3, $requests[1]->getQuery()->get('page_size')); + $this->assertEquals(1, $requests[2]->getQuery()->get('page_size')); + } + + public function testUseAsArray() + { + $this->getServer()->flush(); + $this->getServer()->enqueue(array( + "HTTP/1.1 200 OK\r\nContent-Length: 52\r\n\r\n{ \"next_token\": \"g\", \"resources\": [\"d\", \"e\", \"f\"] }", + "HTTP/1.1 200 OK\r\nContent-Length: 52\r\n\r\n{ \"next_token\": \"\", \"resources\": [\"g\", \"h\", \"i\"] }" + )); + + $ri = new MockCommandIterator($this->getServiceBuilder()->get('mock')->getCommand('iterable_command')); + + // Ensure that the key is never < 0 + $this->assertEquals(0, $ri->key()); + $this->assertEquals(0, count($ri)); + + // Ensure that the iterator can be used as KVP array + $data = array(); + foreach ($ri as $key => $value) { + $data[$key] = $value; + } + + // Ensure that the iterate is countable + $this->assertEquals(6, count($ri)); + $this->assertEquals(array('d', 'e', 'f', 'g', 'h', 'i'), $data); + } + + public function testBailsWhenSendReturnsNoResults() + { + $this->getServer()->flush(); + $this->getServer()->enqueue(array( + "HTTP/1.1 200 OK\r\n\r\n{ \"next_token\": \"g\", \"resources\": [\"d\", \"e\", \"f\"] }", + "HTTP/1.1 200 OK\r\n\r\n{ \"next_token\": \"\", \"resources\": [] }" + )); + + $ri = new MockCommandIterator($this->getServiceBuilder()->get('mock')->getCommand('iterable_command')); + + // Ensure that the iterator can be used as KVP array + $data = $ri->toArray(); + + // Ensure that the iterate is countable + $this->assertEquals(3, count($ri)); + $this->assertEquals(array('d', 'e', 'f'), $data); + + $this->assertEquals(2, $ri->getRequestCount()); + } + + public function testHoldsDataOptions() + { + $ri = new MockCommandIterator($this->getServiceBuilder()->get('mock')->getCommand('iterable_command')); + $this->assertNull($ri->get('foo')); + $this->assertSame($ri, $ri->set('foo', 'bar')); + $this->assertEquals('bar', $ri->get('foo')); + } + + public function testSettingLimitOrPageSizeClearsData() + { + $this->getServer()->flush(); + $this->getServer()->enqueue(array( + "HTTP/1.1 200 OK\r\n\r\n{ \"next_token\": \"\", \"resources\": [\"d\", \"e\", \"f\"] }", + "HTTP/1.1 200 OK\r\n\r\n{ \"next_token\": \"\", \"resources\": [\"d\", \"e\", \"f\"] }", + "HTTP/1.1 200 OK\r\n\r\n{ \"next_token\": \"\", \"resources\": [\"d\", \"e\", \"f\"] }" + )); + + $ri = new MockCommandIterator($this->getServiceBuilder()->get('mock')->getCommand('iterable_command')); + $ri->toArray(); + $this->assertNotEmpty($this->readAttribute($ri, 'resources')); + + $ri->setLimit(10); + $this->assertEmpty($this->readAttribute($ri, 'resources')); + + $ri->toArray(); + $this->assertNotEmpty($this->readAttribute($ri, 'resources')); + $ri->setPageSize(10); + $this->assertEmpty($this->readAttribute($ri, 'resources')); + } + + public function testWorksWithCustomAppendIterator() + { + $this->getServer()->flush(); + $this->getServer()->enqueue(array( + "HTTP/1.1 200 OK\r\n\r\n{ \"next_token\": \"\", \"resources\": [\"d\", \"e\", \"f\"] }" + )); + $ri = new MockCommandIterator($this->getServiceBuilder()->get('mock')->getCommand('iterable_command')); + $a = new \Guzzle\Iterator\AppendIterator(); + $a->append($ri); + $results = iterator_to_array($a, false); + $this->assertEquals(4, $ri->calledNext); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Stream/PhpStreamRequestFactoryTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Stream/PhpStreamRequestFactoryTest.php new file mode 100644 index 000000000..083aaa0d9 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Stream/PhpStreamRequestFactoryTest.php @@ -0,0 +1,172 @@ +client = new Client($this->getServer()->getUrl()); + $this->factory = new PhpStreamRequestFactory(); + } + + public function testOpensValidStreamByCreatingContext() + { + $this->getServer()->enqueue("HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nhi"); + $request = $this->client->get('/'); + $stream = $this->factory->fromRequest($request); + $this->assertEquals('hi', (string) $stream); + $headers = $this->factory->getLastResponseHeaders(); + $this->assertContains('HTTP/1.1 200 OK', $headers); + $this->assertContains('Content-Length: 2', $headers); + $this->assertSame($headers, $stream->getCustomData('response_headers')); + $this->assertEquals(2, $stream->getSize()); + } + + public function testOpensValidStreamByPassingContextAndMerging() + { + $request = $this->client->get('/'); + $this->factory = $this->getMockBuilder('Guzzle\Stream\PhpStreamRequestFactory') + ->setMethods(array('createContext', 'createStream')) + ->getMock(); + $this->factory->expects($this->never()) + ->method('createContext'); + $this->factory->expects($this->once()) + ->method('createStream') + ->will($this->returnValue(new Stream(fopen('php://temp', 'r')))); + + $context = array('http' => array('method' => 'HEAD', 'ignore_errors' => false)); + $this->factory->fromRequest($request, stream_context_create($context)); + $options = stream_context_get_options($this->readAttribute($this->factory, 'context')); + $this->assertEquals('HEAD', $options['http']['method']); + $this->assertFalse($options['http']['ignore_errors']); + $this->assertEquals('1.1', $options['http']['protocol_version']); + } + + public function testAppliesProxySettings() + { + $request = $this->client->get('/'); + $request->getCurlOptions()->set(CURLOPT_PROXY, 'tcp://foo.com'); + $this->factory = $this->getMockBuilder('Guzzle\Stream\PhpStreamRequestFactory') + ->setMethods(array('createStream')) + ->getMock(); + $this->factory->expects($this->once()) + ->method('createStream') + ->will($this->returnValue(new Stream(fopen('php://temp', 'r')))); + $this->factory->fromRequest($request); + $options = stream_context_get_options($this->readAttribute($this->factory, 'context')); + $this->assertEquals('tcp://foo.com', $options['http']['proxy']); + } + + public function testAddsPostFields() + { + $this->getServer()->flush(); + $this->getServer()->enqueue("HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nhi"); + $request = $this->client->post('/', array('Foo' => 'Bar'), array('foo' => 'baz bar')); + $stream = $this->factory->fromRequest($request); + $this->assertEquals('hi', (string) $stream); + + $headers = $this->factory->getLastResponseHeaders(); + $this->assertContains('HTTP/1.1 200 OK', $headers); + $this->assertContains('Content-Length: 2', $headers); + $this->assertSame($headers, $stream->getCustomData('response_headers')); + + $received = $this->getServer()->getReceivedRequests(); + $this->assertEquals(1, count($received)); + $this->assertContains('POST / HTTP/1.1', $received[0]); + $this->assertContains('host: ', $received[0]); + $this->assertContains('user-agent: Guzzle/', $received[0]); + $this->assertContains('foo: Bar', $received[0]); + $this->assertContains('content-length: 13', $received[0]); + $this->assertContains('foo=baz%20bar', $received[0]); + } + + public function testAddsBody() + { + $this->getServer()->flush(); + $this->getServer()->enqueue("HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nhi"); + $request = $this->client->put('/', array('Foo' => 'Bar'), 'Testing...123'); + $stream = $this->factory->fromRequest($request); + $this->assertEquals('hi', (string) $stream); + + $headers = $this->factory->getLastResponseHeaders(); + $this->assertContains('HTTP/1.1 200 OK', $headers); + $this->assertContains('Content-Length: 2', $headers); + $this->assertSame($headers, $stream->getCustomData('response_headers')); + + $received = $this->getServer()->getReceivedRequests(); + $this->assertEquals(1, count($received)); + $this->assertContains('PUT / HTTP/1.1', $received[0]); + $this->assertContains('host: ', $received[0]); + $this->assertContains('user-agent: Guzzle/', $received[0]); + $this->assertContains('foo: Bar', $received[0]); + $this->assertContains('content-length: 13', $received[0]); + $this->assertContains('Testing...123', $received[0]); + } + + public function testCanDisableSslValidation() + { + $request = $this->client->get('/'); + $request->getCurlOptions()->set(CURLOPT_SSL_VERIFYPEER, false); + $this->factory = $this->getMockBuilder('Guzzle\Stream\PhpStreamRequestFactory') + ->setMethods(array('createStream')) + ->getMock(); + $this->factory->expects($this->once()) + ->method('createStream') + ->will($this->returnValue(new Stream(fopen('php://temp', 'r')))); + $this->factory->fromRequest($request); + $options = stream_context_get_options($this->readAttribute($this->factory, 'context')); + $this->assertFalse($options['ssl']['verify_peer']); + } + + public function testUsesSslValidationByDefault() + { + $request = $this->client->get('/'); + $this->factory = $this->getMockBuilder('Guzzle\Stream\PhpStreamRequestFactory') + ->setMethods(array('createStream')) + ->getMock(); + $this->factory->expects($this->once()) + ->method('createStream') + ->will($this->returnValue(new Stream(fopen('php://temp', 'r')))); + $this->factory->fromRequest($request); + $options = stream_context_get_options($this->readAttribute($this->factory, 'context')); + $this->assertTrue($options['ssl']['verify_peer']); + $this->assertSame($request->getCurlOptions()->get(CURLOPT_CAINFO), $options['ssl']['cafile']); + } + + public function testBasicAuthAddsUserAndPassToUrl() + { + $request = $this->client->get('/'); + $request->setAuth('Foo', 'Bar'); + $this->factory = $this->getMockBuilder('Guzzle\Stream\PhpStreamRequestFactory') + ->setMethods(array('createStream')) + ->getMock(); + $this->factory->expects($this->once()) + ->method('createStream') + ->will($this->returnValue(new Stream(fopen('php://temp', 'r')))); + $this->factory->fromRequest($request); + $this->assertContains('Foo:Bar@', (string) $this->readAttribute($this->factory, 'url')); + } + + public function testCanCreateCustomStreamClass() + { + $this->getServer()->enqueue("HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nhi"); + $request = $this->client->get('/'); + $stream = $this->factory->fromRequest($request, array(), array('stream_class' => 'Guzzle\Http\EntityBody')); + $this->assertInstanceOf('Guzzle\Http\EntityBody', $stream); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/Stream/StreamTest.php b/guzzle/guzzle/tests/Guzzle/Tests/Stream/StreamTest.php new file mode 100644 index 000000000..4858accae --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/Stream/StreamTest.php @@ -0,0 +1,189 @@ +assertEquals($handle, $stream->getStream()); + $this->assertTrue($stream->isReadable()); + $this->assertTrue($stream->isWritable()); + $this->assertTrue($stream->isLocal()); + $this->assertTrue($stream->isSeekable()); + $this->assertEquals('PHP', $stream->getWrapper()); + $this->assertEquals('TEMP', $stream->getStreamType()); + $this->assertEquals(4, $stream->getSize()); + $this->assertEquals('php://temp', $stream->getUri()); + $this->assertEquals(array(), $stream->getWrapperData()); + $this->assertFalse($stream->isConsumed()); + unset($stream); + } + + public function testCanModifyStream() + { + $handle1 = fopen('php://temp', 'r+'); + $handle2 = fopen('php://temp', 'r+'); + $stream = new Stream($handle1); + $this->assertSame($handle1, $stream->getStream()); + $stream->setStream($handle2, 10); + $this->assertEquals(10, $stream->getSize()); + $this->assertSame($handle2, $stream->getStream()); + } + + public function testStreamClosesHandleOnDestruct() + { + $handle = fopen('php://temp', 'r'); + $stream = new Stream($handle); + unset($stream); + $this->assertFalse(is_resource($handle)); + } + + public function testConvertsToString() + { + $handle = fopen('php://temp', 'w+'); + fwrite($handle, 'data'); + $stream = new Stream($handle); + $this->assertEquals('data', (string) $stream); + unset($stream); + + $handle = fopen(__DIR__ . '/../TestData/FileBody.txt', 'r'); + $stream = new Stream($handle); + $this->assertEquals('', (string) $stream); + unset($stream); + } + + public function testConvertsToStringAndRestoresCursorPos() + { + $handle = fopen('php://temp', 'w+'); + $stream = new Stream($handle); + $stream->write('foobazbar'); + $stream->seek(3); + $this->assertEquals('foobazbar', (string) $stream); + $this->assertEquals(3, $stream->ftell()); + } + + public function testIsConsumed() + { + $handle = fopen('php://temp', 'w+'); + fwrite($handle, 'data'); + $stream = new Stream($handle); + $this->assertFalse($stream->isConsumed()); + $stream->read(4); + $this->assertTrue($stream->isConsumed()); + } + + public function testAllowsSettingManualSize() + { + $handle = fopen('php://temp', 'w+'); + fwrite($handle, 'data'); + $stream = new Stream($handle); + $stream->setSize(10); + $this->assertEquals(10, $stream->getSize()); + unset($stream); + } + + public function testWrapsStream() + { + $handle = fopen('php://temp', 'w+'); + fwrite($handle, 'data'); + $stream = new Stream($handle); + $this->assertTrue($stream->isSeekable()); + $this->assertTrue($stream->isReadable()); + $this->assertTrue($stream->seek(0)); + $this->assertEquals('da', $stream->read(2)); + $this->assertEquals('ta', $stream->read(2)); + $this->assertTrue($stream->seek(0)); + $this->assertEquals('data', $stream->read(4)); + $stream->write('_appended'); + $stream->seek(0); + $this->assertEquals('data_appended', $stream->read(13)); + } + + public function testGetSize() + { + $size = filesize(__DIR__ . '/../../../bootstrap.php'); + $handle = fopen(__DIR__ . '/../../../bootstrap.php', 'r'); + $stream = new Stream($handle); + $this->assertEquals($handle, $stream->getStream()); + $this->assertEquals($size, $stream->getSize()); + $this->assertEquals($size, $stream->getSize()); + unset($stream); + + // Make sure that false is returned when the size cannot be determined + $this->getServer()->enqueue("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); + $handle = fopen('http://localhost:' . $this->getServer()->getPort(), 'r'); + $stream = new Stream($handle); + $this->assertEquals(false, $stream->getSize()); + unset($stream); + } + + public function testEnsuresSizeIsConsistent() + { + $h = fopen('php://temp', 'r+'); + fwrite($h, 'foo'); + $stream = new Stream($h); + $this->assertEquals(3, $stream->getSize()); + $stream->write('test'); + $this->assertEquals(7, $stream->getSize()); + fclose($h); + } + + public function testAbstractsMetaData() + { + $handle = fopen(__DIR__ . '/../../../bootstrap.php', 'r'); + $stream = new Stream($handle); + $this->assertEquals('plainfile', $stream->getMetaData('wrapper_type')); + $this->assertEquals(null, $stream->getMetaData('wrapper_data')); + $this->assertInternalType('array', $stream->getMetaData()); + } + + public function testDoesNotAttemptToWriteToReadonlyStream() + { + $handle = fopen(__DIR__ . '/../../../bootstrap.php', 'r'); + $stream = new Stream($handle); + $this->assertEquals(0, $stream->write('foo')); + } + + public function testProvidesStreamPosition() + { + $handle = fopen(__DIR__ . '/../../../bootstrap.php', 'r'); + $stream = new Stream($handle); + $stream->read(2); + $this->assertSame(ftell($handle), $stream->ftell()); + $this->assertEquals(2, $stream->ftell()); + } + + public function testRewindIsSeekZero() + { + $stream = new Stream(fopen('php://temp', 'w+')); + $stream->write('foobazbar'); + $this->assertTrue($stream->rewind()); + $this->assertEquals('foobazbar', $stream->read(9)); + } + + public function testCanDetachStream() + { + $r = fopen('php://temp', 'w+'); + $stream = new Stream($r); + $stream->detachStream(); + $this->assertNull($stream->getStream()); + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/TestData/FileBody.txt b/guzzle/guzzle/tests/Guzzle/Tests/TestData/FileBody.txt new file mode 100644 index 000000000..e69de29bb diff --git a/guzzle/guzzle/tests/Guzzle/Tests/TestData/description/bar.json b/guzzle/guzzle/tests/Guzzle/Tests/TestData/description/bar.json new file mode 100644 index 000000000..c354ed788 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/TestData/description/bar.json @@ -0,0 +1,3 @@ +{ + "includes": ["foo.json"] +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/TestData/description/baz.json b/guzzle/guzzle/tests/Guzzle/Tests/TestData/description/baz.json new file mode 100644 index 000000000..765237b3e --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/TestData/description/baz.json @@ -0,0 +1,3 @@ +{ + "includes": ["foo.json", "bar.json"] +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/TestData/description/foo.json b/guzzle/guzzle/tests/Guzzle/Tests/TestData/description/foo.json new file mode 100644 index 000000000..cee5005b8 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/TestData/description/foo.json @@ -0,0 +1,8 @@ +{ + "includes": ["recursive.json"], + "operations": { + "abstract": { + "httpMethod": "POST" + } + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/TestData/description/recursive.json b/guzzle/guzzle/tests/Guzzle/Tests/TestData/description/recursive.json new file mode 100644 index 000000000..c354ed788 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/TestData/description/recursive.json @@ -0,0 +1,3 @@ +{ + "includes": ["foo.json"] +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/TestData/mock_response b/guzzle/guzzle/tests/Guzzle/Tests/TestData/mock_response new file mode 100644 index 000000000..b6938a28c --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/TestData/mock_response @@ -0,0 +1,3 @@ +HTTP/1.1 200 OK +Content-Length: 0 + diff --git a/guzzle/guzzle/tests/Guzzle/Tests/TestData/services/json1.json b/guzzle/guzzle/tests/Guzzle/Tests/TestData/services/json1.json new file mode 100644 index 000000000..7b2a9dad4 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/TestData/services/json1.json @@ -0,0 +1,18 @@ +{ + "includes": [ "json2.json" ], + "services": { + "abstract": { + "access_key": "xyz", + "secret": "abc" + }, + "mock": { + "class": "Guzzle\\Tests\\Service\\Mock\\MockClient", + "extends": "abstract", + "params": { + "username": "foo", + "password": "baz", + "subdomain": "bar" + } + } + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/TestData/services/json2.json b/guzzle/guzzle/tests/Guzzle/Tests/TestData/services/json2.json new file mode 100644 index 000000000..08e5566f0 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/TestData/services/json2.json @@ -0,0 +1,11 @@ +{ + "services": { + "foo": { + "class": "Guzzle\\Tests\\Service\\Mock\\MockClient", + "extends": "abstract", + "params": { + "baz": "bar" + } + } + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/TestData/services/services.json b/guzzle/guzzle/tests/Guzzle/Tests/TestData/services/services.json new file mode 100644 index 000000000..25452e4a4 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/TestData/services/services.json @@ -0,0 +1,71 @@ +{ + "abstract": { + "access_key": "xyz", + "secret": "abc" + }, + "mock": { + "class": "Guzzle\\Tests\\Service\\Mock\\MockClient", + "extends": "abstract", + "params": { + "username": "foo", + "password": "baz", + "subdomain": "bar" + } + }, + + "test.abstract.aws": { + "params": { + "access_key": "12345", + "secret_key": "abcd" + } + }, + + "test.s3": { + "class": "Guzzle\\Service\\Aws\\S3Client", + "extends": "test.abstract.aws", + "params": { + "devpay_product_token": "", + "devpay_user_token": "" + } + }, + + "test.simple_db": { + "class": "Guzzle\\Service\\Aws\\SimpleDb\\SimpleDbClient", + "extends": "test.abstract.aws" + }, + + "test.sqs": { + "class": "Guzzle\\Service\\Aws\\Sqs\\SqsClient", + "extends": "test.abstract.aws" + }, + + "test.centinel": { + "class": "Guzzle\\Service\\CardinalCommerce\\Centinel.CentinelClient", + "params": { + "password": "test", + "processor_id": "123", + "merchant_id": "456" + } + }, + + "test.mws": { + "class": "Guzzle\\Service\\Mws\\MwsClient", + "extends": "test.abstract.aws", + "params": { + "merchant_id": "ABCDE", + "marketplace_id": "FGHIJ", + "application_name": "GuzzleTest", + "application_version": "0.1", + "base_url": "https://mws.amazonservices.com" + } + }, + + "mock": { + "class": "Guzzle\\Tests\\Service\\Mock\\MockClient", + "params": { + "username": "test_user", + "password": "****", + "subdomain": "test" + } + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/TestData/test_service.json b/guzzle/guzzle/tests/Guzzle/Tests/TestData/test_service.json new file mode 100644 index 000000000..01557ca11 --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/TestData/test_service.json @@ -0,0 +1,40 @@ +{ + "includes": [ "test_service2.json" ], + "operations": { + "test": { + "uri": "/path" + }, + "concrete": { + "extends": "abstract" + }, + "foo_bar": { + "uri": "/testing", + "parameters": { + "other": { + "location": "json", + "location_key": "Other" + }, + "test": { + "type": "object", + "location": "json", + "properties": { + "baz": { + "type": "boolean", + "default": true + }, + "bar": { + "type": "string", + "filters": [ + { + "method": "strtolower", + "args": ["test", "@value"] + }, + "strtoupper" + ] + } + } + } + } + } + } +} diff --git a/guzzle/guzzle/tests/Guzzle/Tests/TestData/test_service2.json b/guzzle/guzzle/tests/Guzzle/Tests/TestData/test_service2.json new file mode 100644 index 000000000..66dd9ef7e --- /dev/null +++ b/guzzle/guzzle/tests/Guzzle/Tests/TestData/test_service2.json @@ -0,0 +1,7 @@ +{ + "operations": { + "abstract": { + "uri": "/abstract" + } + } +} diff --git a/guzzle/guzzle/tests/bootstrap.php b/guzzle/guzzle/tests/bootstrap.php new file mode 100644 index 000000000..d5f8cb627 --- /dev/null +++ b/guzzle/guzzle/tests/bootstrap.php @@ -0,0 +1,10 @@ +getMessage() . "\n"); + print_test($test); + print("\n"); + return false; + } + else + { + if ($verbose) + { + if (array_key_exists('comment', $test)) + { + print "OK: " . $test['comment'] . "\n"; + } + print("caught: " . $ex->getMessage() . "\n"); + print("expected: " . $test['error'] . "\n\n"); + } + return true; + } + } +} + + +// Piggyback on patch tests to test diff as well - use 'doc' and +// 'expected' from testcases. Generate a diff, apply it, and check +// that it matches the target - in both directions. +function diff_test($test) +{ + // Skip comment-only or test op tests + if (!(isset($test['doc']) && isset($test['expected']))) + { + return true; + } + + $result = true; + try + { + $doc1 = $test['doc']; // copy, in case sort/patch alters + $doc2 = $test['expected']; + $patch = JsonPatch::diff($doc1, $doc2); + $patched = JsonPatch::patch($doc1, $patch); + if (!JsonPatch::considered_equal($patched, $doc2)) + { + print("diff test failed:\n"); + print_test($test); + print("from: " . json_encode($doc1) . "\n"); + print("diff: " . json_encode($patch) . "\n"); + print("found: " . json_encode($patched) . "\n"); + print("expected: " . json_encode($doc2) . "\n\n"); + $result = false; + } + + // reverse order + $doc1 = $test['expected']; // copy, in case sort/patch alters + $doc2 = $test['doc']; + $patch = JsonPatch::diff($doc1, $doc2); + $patched = JsonPatch::patch($doc1, $patch); + if (!JsonPatch::considered_equal($patched, $doc2)) + { + print("reverse diff test failed:\n"); + print_test($test); + print("from: " . json_encode($doc1) . "\n"); + print("diff: " . json_encode($patch) . "\n"); + print("found: " . json_encode($patched) . "\n"); + print("expected: " . json_encode($doc2) . "\n\n"); + $result = false; + } + } + catch (Exception $ex) + { + print("caught exception ".$ex->getMessage()."\n"); + return false; + } + return $result; +} + + +function test_file($filename, $simplexml_mode=false) +{ + $testfile = file_get_contents($filename); + if (!$testfile) + { + throw new Exception("Couldn't find test file $filename"); + return false; + } + + $tests = json_decode($testfile, 1); + if (is_null($tests)) + { + throw new Exception("Error json-decoding test file $filename"); + } + + $success = true; + foreach ($tests as $test) + { + if (isset($test['disabled']) && $test['disabled']) + { + continue; + } + if (!do_test($test, $simplexml_mode)) + { + $success = false; + } + if (!$simplexml_mode && !diff_test($test)) + { + $success = false; + } + } + return $success; +} + + +function main() +{ + $result = true; + $testfiles = array( + 'local_tests.json', + 'json-patch-tests/tests.json', + 'json-patch-tests/spec_tests.json' + ); + foreach ($testfiles as $testfile) + { + if (!test_file($testfile)) + { + $result = false; + } + } + if (!test_file('simplexml_tests.json', true)) + { + $result = false; + } + return $result; +} + + +if (!main()) +{ + exit(1); +} +else +{ + exit(0); +} \ No newline at end of file diff --git a/mikemccabe/json-patch-php/simplexml_tests.json b/mikemccabe/json-patch-php/simplexml_tests.json new file mode 100644 index 000000000..d2e290586 --- /dev/null +++ b/mikemccabe/json-patch-php/simplexml_tests.json @@ -0,0 +1,65 @@ +[ + { "comment": "simplexml promotion - add after scalar", + "doc": { "foo":1 }, + "patch": [ { "op":"add", "path":"/foo/1", "value":2 } ], + "expected": { "foo":[1, 2] } }, + + { "comment": "simplexml promotion - add before scalar", + "doc": { "foo":1 }, + "patch": [ { "op":"add", "path":"/foo/0", "value":2 } ], + "expected": { "foo":[2, 1] } }, + + { "comment": "simplexml promotion - append", + "doc": { "foo":1 }, + "patch": [ { "op":"add", "path":"/foo/-", "value":2 } ], + "expected": { "foo":[1, 2] } }, + + { "comment": "append to array", + "doc": { "foo":1 }, + "patch": [ { "op":"add", "path":"/foo/-", "value":2 } ], + "expected": { "foo":[1, 2] } }, + + { "comment": "mid-path 0-index with tail 0-index - append", + "doc": { "foo": { "bar": 1 } }, + "patch": [ { "op":"add", "path":"/foo/0/bar/-", "value":2 }], + "expected": { "foo": { "bar": [1,2] }} }, + + { "comment": "Add 1-length array is equivalent to scalar add", + "doc": { }, + "patch": [ { "op":"add", "path":"/foo/0", "value":1 } ], + "expected": { "foo":1 }, + "disabled": true + }, + + { "comment": "simple 0-index of scalar ok", + "doc": { "foo": 1 }, + "patch": [ { "op":"test", "path":"/foo/0", "value":1 }] }, + + { "comment": "nested 0-index of scalar ok", + "doc": { "foo": { "bar": 1 } }, + "patch": [ { "op":"test", "path":"/foo/bar/0", "value":1 }] }, + + { "comment": "0-index after actual 0-index ok", + "doc": { "foo": [{ "bar": 1 }, 1] }, + "patch": [ { "op":"test", "path":"/foo/0/bar/0", "value":1 }] }, + + { "comment": "mid-path 0-index", + "doc": { "foo": { "bar": [1, 2] } }, + "patch": [ { "op":"test", "path":"/foo/0/bar/0", "value":1 }] }, + + { "comment": "mid-path 0-index with tail 0-index", + "doc": { "foo": { "bar": 1 } }, + "patch": [ { "op":"test", "path":"/foo/0/bar/0", "value":1 }] }, + + { "comment": "replace as array", + "doc": { "foo":1 }, + "patch": [ { "op":"replace", "path":"/foo/0", "value":2 } ], + "expected": { "foo":2 } }, + + { "comment": "remove last demotes to singleton", + "doc": { "foo":[1, 2] }, + "patch": [ { "op":"remove", "path":"/foo/1"} ], + "expected": { "foo":1 } }, + + { "comment": "tests complete" } +] diff --git a/mikemccabe/json-patch-php/src/JsonPatch.php b/mikemccabe/json-patch-php/src/JsonPatch.php new file mode 100644 index 000000000..d180e5a5e --- /dev/null +++ b/mikemccabe/json-patch-php/src/JsonPatch.php @@ -0,0 +1,600 @@ + "replace", "path" => "$path", + "value" => $other)); + } + } + return array(); + } + + + // Walk associative arrays $src and $dst, returning a list of patches + private static function diff_assoc($path, $src, $dst) + { + $result = array(); + if (count($src) == 0 && count($dst) != 0) + { + $result[] = array("op" => "replace", "path" => "$path", "value" => $dst); + } + else + { + foreach (array_keys($src) as $key) + { + $ekey = self::escape_pointer_part($key); + if (!array_key_exists($key, $dst)) + { + $result[] = array("op" => "remove", "path" => "$path/$ekey"); + } + else + { + $result = array_merge($result, + self::diff_values("$path/$ekey", + $src[$key], $dst[$key])); + } + } + foreach (array_keys($dst) as $key) + { + if (!array_key_exists($key, $src)) + { + $ekey = self::escape_pointer_part($key); + $result[] = array("op" => "add", "path" => "$path/$ekey", + "value" => $dst[$key]); + } + } + } + return $result; + } + + + // Walk simple arrays $src and $dst, returning a list of patches + private static function diff_array($path, $src, $dst) + { + $result = array(); + $lsrc = count($src); + $ldst = count($dst); + $max = ($lsrc > $ldst) ? $lsrc : $ldst; + + // Walk backwards through arrays, starting with longest + $i = $max - 1; + while ($i >= 0) // equivalent for loop didn't work? + { + if ($i < $lsrc && $i < $ldst && + array_key_exists($i, $src) && array_key_exists($i, $dst)) + { + $result = array_merge($result, + self::diff_values("$path/$i", + $src[$i], $dst[$i])); + } + else if ($i < $ldst && array_key_exists($i, $dst)) + { + $result[] = array("op" => "add", "path" => "$path/$i", + "value" => $dst[$i]); + } + else if ($i < $lsrc && !array_key_exists($i, $dst)) + { + $result[] = array("op" => "remove", "path" => "$path/$i"); + } + $i--; + } + return $result; + } + + + // patch support functions + + + // Implements the 'test' op + private static function test($doc, $path, $parts, $value, $simplexml_mode) + { + $found = self::get_helper($doc, $path, $parts, $simplexml_mode); + + if (!self::considered_equal($found, $value)) + { + throw new JsonPatchException("test target value different - expected " + . json_encode($value) . ", found " + . json_encode($found)); + } + } + + + // Helper for get() and 'copy', 'move', 'test' ops - get a value from a doc. + private static function get_helper($doc, $path, $parts, $simplexml_mode) + { + if (count($parts) == 0) + { + return $doc; + } + + $part = array_shift($parts); + if (!is_array($doc) || !array_key_exists($part, $doc)) + { + throw new JsonPatchException("Path '$path' not found"); + } + if ($simplexml_mode + && count($parts) > 0 + && $parts[0] == '0' + && self::is_associative($doc) + && !(is_array($doc[$part]) && !self::is_associative($doc[$part]))) + { + return self::get_helper(array($doc[$part]), $path, $parts, + $simplexml_mode); + } + else + { + return self::get_helper($doc[$part], $path, $parts, + $simplexml_mode); + } + } + + + // Test whether a php array looks 'associative' - does it have + // any non-numeric keys? + // + // note: is_associative(array()) === false + private static function is_associative($a) + { + if (!is_array($a)) + { + return false; + } + foreach (array_keys($a) as $key) + { + if (is_string($key)) + { + return true; + } + } + // Also treat php gappy arrays as associative. + // (e.g. {"0":"a", "2":"c"}) + $len = count($a); + for ($i = 0; $i < $len; $i++) + { + if (!array_key_exists($i, $a)) + { + return true; + } + } + return false; + } + + // Recursively sort array keys + private static function rksort($a) + { + if (!is_array($a)) + { + return $a; + } + foreach (array_keys($a) as $key) + { + $a[$key] = self::rksort($a[$key]); + } + // SORT_STRING seems required, as otherwise numeric indices + // (e.g. "4") aren't sorted. + ksort($a, SORT_STRING); + return $a; + } + + + // Per http://tools.ietf.org/html/rfc6902#section-4.6 + public static function considered_equal($a1, $a2) + { + return json_encode(self::rksort($a1)) === json_encode(self::rksort($a2)); + } + + + // Apply a single op to modify the given document. + // + // As php arrays are not passed by reference, this function works + // recursively, rebuilding complete subarrays that need changing; + // the revised subarray is changed in the parent array before + // returning it. + private static function do_op($doc, $op, $path, $parts, $value, + $simplexml_mode) + { + // Special-case toplevel + if (count($parts) == 0) + { + if ($op == 'add' || $op == 'replace') + { + return $value; + } + else if ($op == 'remove') + { + throw new JsonPatchException("Can't remove whole document"); + } + else + { + throw new JsonPatchException("'$op' can't operate on whole document"); + } + } + + $part = array_shift($parts); + + // recur until we get to the target + if (count($parts) > 0) + { + if (!array_key_exists($part, $doc)) + { + throw new JsonPatchException("Path '$path' not found"); + } + // recur, adding resulting sub-doc into doc returned to caller + + // special case for simplexml-style behavior - make singleton + // scalar leaves look like 1-length arrays + if ($simplexml_mode + && count($parts) > 0 + && ($parts[0] == '0' || $parts[0] == '1' || $parts[0] == '-') + && self::is_associative($doc) + && !(is_array($doc[$part]) && !self::is_associative($doc[$part]))) + { + $doc[$part] = self::do_op(array($doc[$part]), $op, $path, $parts, + $value, $simplexml_mode); + } + else + { + $doc[$part] = self::do_op($doc[$part], $op, $path, $parts, + $value, $simplexml_mode); + } + return $doc; + } + + // at target + if (!is_array($doc)) + { + throw new JsonPatchException('Target must be array or associative array'); + } + + if (!self::is_associative($doc)) // N.B. returns false for empty arrays + { + if (count($doc) && !self::is_index($part) + && !($part == '-' && ($op == 'add' || $op == 'append'))) + { + throw new JsonPatchException("Non-array key '$part' used on array"); + } + else + { + // check range, if numeric + if (self::is_index($part) && + ($part < 0 || (($op == 'remove' && $part >= count($doc)) + || ($op != 'remove' && $part > count($doc))))) + { + throw new JsonPatchException("Can't operate outside of array bounds"); + } + } + } + + if ($op == 'add' || $op == 'append') + { + if (!self::is_associative($doc) + && (self::is_index($part) || $part == '-')) + { + // If index is '-', use array length + $index = ($part == '-') ? count($doc) : $part; + if ($op == 'append') + { + array_splice($doc, $index, 0, $value); + } + else + { + array_splice($doc, $index, 0, Array($value)); + } + } + else + { + $doc[$part] = $value; + } + } + + else if ($op == 'replace') + { + if (!self::is_associative($doc) && self::is_index($part)) + { + array_splice($doc, $part, 1, Array($value)); + } + else + { + if (!array_key_exists($part, $doc)) + { + throw new JsonPatchException("replace target '$path' not set"); + } + $doc[$part] = $value; + } + } + + else if ($op == 'remove') + { + if (!self::is_associative($doc) && self::is_index($part)) + { + array_splice($doc, $part, 1); + } + else + { + if (!array_key_exists($part, $doc)) + { + throw new JsonPatchException("remove target '$path' not set"); + } + unset($doc[$part]); + } + } + return $doc; + } +} diff --git a/rackspace/php-opencloud/CODE_OF_CONDUCT.md b/rackspace/php-opencloud/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..0832bf8bb --- /dev/null +++ b/rackspace/php-opencloud/CODE_OF_CONDUCT.md @@ -0,0 +1,50 @@ +# Contributor Code of Conduct + +As contributors and maintainers of this project, and in the interest of +fostering an open and welcoming community, we pledge to respect all people who +contribute through reporting issues, posting feature requests, updating +documentation, submitting pull requests or patches, and other activities. + +We are committed to making participation in this project a harassment-free +experience for everyone, regardless of level of experience, gender, gender +identity and expression, sexual orientation, disability, personal appearance, +body size, race, ethnicity, age, religion, or nationality. + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery +* Personal attacks +* Trolling or insulting/derogatory comments +* Public or private harassment +* Publishing other's private information, such as physical or electronic + addresses, without explicit permission +* Other unethical or unprofessional conduct + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +By adopting this Code of Conduct, project maintainers commit themselves to +fairly and consistently applying these principles to every aspect of managing +this project. Project maintainers who do not follow or enforce the Code of +Conduct may be permanently removed from the project team. + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting a project maintainer at sdk-support@rackspace.com. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. Maintainers are +obligated to maintain confidentiality with regard to the reporter of an +incident. + + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 1.3.0, available at +[http://contributor-covenant.org/version/1/3/0/][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/3/0/ diff --git a/rackspace/php-opencloud/CONTRIBUTING.md b/rackspace/php-opencloud/CONTRIBUTING.md index f638265c4..4764be882 100644 --- a/rackspace/php-opencloud/CONTRIBUTING.md +++ b/rackspace/php-opencloud/CONTRIBUTING.md @@ -11,13 +11,21 @@ efforts. Here are a few general guidelines to follow: will need to write new test cases; if you're updating existing code, you will need to make sure the methods you're updating are still completely covered. -3. Please abide by PSR code styling. +3. Please abide by [PSR-2 code styling](#ensuring-psr-2-coding-style-compliance). 4. Explaining your pull requests is appreciated. Unless you're fixing a minor typographical error, create a description which explains your changes and, where relevant, references the existing issue you're hoping to fix. -5. Document your code! +5. If your pull request introduces a large change or addition, please consider + creating a work-in-progress (WIP) pull request. This lets us review your changes + and provide feedback early and often rather than all at once when the entire pull + request is ready. To denote a pull request as WIP, simply add the "PR: Work In Progress" + label to it. When you are finished with your work in the pull request and + are ready for a final review and merge, please remove the "PR: Work In Progress" + label. + +6. Document your code! If you submit code, please add your name and email address to the CONTRIBUTORS file. @@ -27,10 +35,39 @@ Test Instructions ### To run unit tests: ```bash -phpunit +vendor/bin/phpunit ``` ### To run the full suite of acceptance tests: 1. Make sure your [variables-order](http://www.php.net/manual/en/ini.core.php#ini.variables-order) is set to "EGCRS" 2. Set your *PHP_OpenCloud_USERNAME* and *PHP_OpenCloud_API_KEY* variables 3. Run: ```php tests/OpenCloud/Smoke/Runner.php``` + +## Conventions + +* When working on a `Service` class (e.g. [`OpenCloud\Image\Service`](/lib/OpenCloud/Image/Service.php), name methods like so: + + * Methods that return a single resource, say `Foo`, should be named `getFoo`. For example, [`getImage`](/lib/OpenCloud/Image/Service.php#L67). + * Methods that return a collection of resources, say `Foo`, should be named `listFoos`. For example, [`listImages`](/lib/OpenCloud/Image/Service.php#L53). + * Methods that create a new resource, say `Foo`, should be named `createFoo`. For example, [`createEntity`](/lib/OpenCloud/CloudMonitoring/Service.php#L105). + +* When validating arguments to a method, please throw `\InvalidArgumentException` when an invalid argument is found. For example, see [here](/lib/OpenCloud/LoadBalancer/Resource/LoadBalancer.php#L212-L215). + +## Ensuring PSR-2 coding style compliance + +The code in this library is compliant with the [PSR-2 Coding Style Guide](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md). To ensure that any code you contribute is also PSR-2 compliant, please run the following command from the base directory of this project _before_ submitting your contribution: + + $ vendor/bin/php-cs-fixer fix --level psr2 . + +Running this command will _change_ your code to become PSR-2 compliant. You will need to _commit_ these changes and make them part of your pull request. + +## Releasing a new version of php-opencloud +If you are a core contributor to php-opencloud, you have the power to release new versions of it. Here are the steps to follow to ensure a proper release: + +1. Update the value of the the [`VERSION` constant](/lib/OpenCloud/Version.php#L30). +2. Merge the `working` branch into the `master` branch. +3. [Run the smoke tests](#to-run-the-full-suite-of-acceptance-tests). If they fail, make necessary changes and go to step 2. +4. [Create new release notes](https://github.com/rackspace/php-opencloud/releases/new). +5. Publish release notes. +6. Announce release via appropriate channels. +7. Party :tada: :balloon: \ No newline at end of file diff --git a/rackspace/php-opencloud/README.md b/rackspace/php-opencloud/README.md index 7aec4dd57..12fb530b8 100644 --- a/rackspace/php-opencloud/README.md +++ b/rackspace/php-opencloud/README.md @@ -4,7 +4,7 @@ PHP SDK for OpenStack/Rackspace APIs [![Latest Stable Version](https://poser.pugx.org/rackspace/php-opencloud/v/stable.png)](https://packagist.org/packages/rackspace/php-opencloud) [![Travis CI](https://secure.travis-ci.org/rackspace/php-opencloud.png)](https://travis-ci.org/rackspace/php-opencloud) [![Total Downloads](https://poser.pugx.org/rackspace/php-opencloud/downloads.png)](https://packagist.org/packages/rackspace/php-opencloud) -For SDKs in different languages, see http://developer.rackspace.com. +Our official documentation is now available on http://docs.php-opencloud.com. For SDKs in different languages, see http://developer.rackspace.com. The PHP SDK should work with most OpenStack-based cloud deployments, though it specifically targets the Rackspace public cloud. In @@ -16,9 +16,11 @@ so that you can still use the SDK with a pure OpenStack instance Requirements ------------ -* PHP >=5.3.3 +* PHP >=5.4 * cURL extension for PHP +**Note**: Since PHP 5.3 has reached [end of life](http://php.net/eol.php) and is no longer officially supported, we are moving to 5.4 as a minimum requirement. If upgrading is not an option and you still need a stable version of the SDK for 5.3, please follow [this guide](http://docs.php-opencloud.com/en/latest/using-php-5.3.html). + Installation ------------ You must install this library through Composer: @@ -28,16 +30,19 @@ You must install this library through Composer: curl -sS https://getcomposer.org/installer | php # Require php-opencloud as a dependency -php composer.phar require rackspace/php-opencloud:dev-master +php composer.phar require rackspace/php-opencloud ``` Once you have installed the library, you will need to load Composer's autoloader (which registers all the required -namespaces): +namespaces). To do this, place the following line of PHP code at the top of your application's PHP files: ```php require 'vendor/autoload.php'; ``` +**Note**: this assumes your application's PHP files are located in the same folder as `vendor/`. If your files are located +elsewhere, please supply the path to `vendor/autoload.php` in the `require` statement above. + And you're ready to go! You can also check out the [Getting Started guide](docs/getting-started.md) for a quick tutorial. @@ -60,11 +65,11 @@ Your feedback is appreciated! If you have specific problems or bugs with this SD also have a [mailing list](https://groups.google.com/forum/#!forum/php-opencloud), so feel free to join to keep up to date with all the latest changes and announcements to the library. -For general feedback and support requests, send an email to sdk-support@rackspace.com. +For general feedback and support requests, contact us at https://developer.rackspace.com/support/ You can also find assistance via IRC on #rackspace at freenode.net. Contributing ------------ If you'd like to contribute to the project, or require help running the unit/acceptance tests, please view the -[contributing guidelines](https://github.com/rackspace/php-opencloud/blob/master/CONTRIBUTING.md). \ No newline at end of file +[contributing guidelines](https://github.com/rackspace/php-opencloud/blob/master/CONTRIBUTING.md). diff --git a/rackspace/php-opencloud/apigen.neon b/rackspace/php-opencloud/apigen.neon new file mode 100644 index 000000000..1907d65f4 --- /dev/null +++ b/rackspace/php-opencloud/apigen.neon @@ -0,0 +1,3 @@ +templateTheme: bootstrap + +accessLevels: [public] diff --git a/rackspace/php-opencloud/composer.json b/rackspace/php-opencloud/composer.json index e338e697f..68d43364b 100644 --- a/rackspace/php-opencloud/composer.json +++ b/rackspace/php-opencloud/composer.json @@ -3,30 +3,36 @@ "description": "PHP SDK for Rackspace/OpenStack APIs", "keywords": ["rackspace", "openstack", "opencloud", "swift", "nova"], "type": "library", - "license": "MIT", + "license": "Apache-2.0", "authors": [ { "name": "Jamie Hannaford", "email": "jamie.hannaford@rackspace.com", "homepage" : "https://github.com/jamiehannaford" - }, - { - "name": "Glen Campbell", - "email": "glen.campbell@rackspace.com" } ], "autoload": { "psr-0": { - "OpenCloud": ["lib/", "tests/"] + "OpenCloud": ["lib/"] + } + }, + "autoload-dev": { + "psr-0": { + "OpenCloud": ["tests/"] } }, "require": { - "php" : ">=5.3.3", - "guzzle/http" : "3.8.*@dev" + "php" : ">=5.4", + "guzzle/guzzle" : "~3.8", + "psr/log": "~1.0", + "mikemccabe/json-patch-php": "~0.1" }, "require-dev" : { - "guzzle/guzzle": "dev-master", - "psr/log": "1.0.*", - "satooshi/php-coveralls": "0.6.*@dev" + "phpunit/phpunit": "4.3.*", + "phpspec/prophecy": "~1.4", + "satooshi/php-coveralls": "0.6.*@dev", + "jakub-onderka/php-parallel-lint": "0.*", + "fabpot/php-cs-fixer": "1.0.*@dev", + "apigen/apigen": "~4.0" } -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/docs/changelog/1.7.0.md b/rackspace/php-opencloud/docs/changelog/1.7.0.md index 482f5950b..8e22e3f9e 100644 --- a/rackspace/php-opencloud/docs/changelog/1.7.0.md +++ b/rackspace/php-opencloud/docs/changelog/1.7.0.md @@ -20,7 +20,7 @@ For a complete list of features, [view the official page](http://guzzlephp.org). ## Keypair support -Although code now exists in the SDK for API compatability for creating servers with keypairs, there has been +Although code now exists in the SDK for API compatibility for creating servers with keypairs, there has been a recent bug in Nova where certain endpoints (DFW, IAD and ORD) do not exhibit the desired functionality. A patch will be released in the near future. For reference, you just need to add in an extra array key: diff --git a/rackspace/php-opencloud/docs/generate.sh b/rackspace/php-opencloud/docs/generate.sh index 01a43a58e..2c88f3eca 100644 --- a/rackspace/php-opencloud/docs/generate.sh +++ b/rackspace/php-opencloud/docs/generate.sh @@ -1,25 +1,43 @@ -#!/bin/bash -# (c)2013 Rackspace Hosting. See COPYING for license. +#!/bin/sh -DOC_DIR=docs/api -LIB_DIR=lib -BIN_FILE=vendor/bin/apigen.php +# Script to be used by Travis CI builds to generate the php-opencloud SDK API +# reference and publish it to the gh-pages branch of the rackerlabs/php-opencloud +# repository. -if [ ! -f $BIN_FILE ]; then - rm composer.lock - php composer.phar require apigen/apigen:dev-master --dev -fi +SOURCE_DIR=lib +WORK_DIR=build/api +API_DOCS_DIR=docs/api +REPO_REMOTE_URL=https://$GH_TOKEN@github.com/rackspace/php-opencloud -if [ ! -d $DOC_DIR ]; then - mkdir $DOC_DIR +# We want our generated API reference to reflect what is +# on the master branch. So if we aren't currently on +# the master branch, or we aren't part of a PR targetted +# to the master branch, do nothing. +if [ "$TRAVIS_BRANCH" != "master" ]; then + exit 0 fi -if [ ! -d docs ]; then - echo "No docs/ directory found; run this script from the top directory" - exit; +# Generate the API references +./vendor/bin/apigen generate \ + --source $SOURCE_DIR \ + --destination $WORK_DIR + +# Switch the branch to gh-pages +git checkout gh-pages +git pull --commit $REPO_REMOTE_URL gh-pages + +if [ $? -ne 0 ]; then + exit 1 fi -rm -rf DOCS_DIR +# Commit the generated API references +rm -rf $API_DOCS_DIR +mv $WORK_DIR $API_DOCS_DIR +git add -f $API_DOCS_DIR +git commit -m "Re-generated API documentation" + +# Push to the remote gh-pages branch so +# changes show up on php-opencloud.com +git push $REPO_REMOTE_URL gh-pages -# regenerate all the docs! -php $BIN_FILE -s $LIB_DIR -d $DOC_DIR --title="PHP OpenCloud API" --groups="namespaces" --download --progressbar \ No newline at end of file +git checkout master diff --git a/rackspace/php-opencloud/docs/getting-started-openstack.md b/rackspace/php-opencloud/docs/getting-started-openstack.md index a76134fe7..1133b948d 100644 --- a/rackspace/php-opencloud/docs/getting-started-openstack.md +++ b/rackspace/php-opencloud/docs/getting-started-openstack.md @@ -1,190 +1,5 @@ # Installing the SDK -You must install through Composer, because this library has a few dependencies: +Our docs have moved! Please visit the below link: -```bash -# Install Composer -curl -sS https://getcomposer.org/installer | php - -# Require php-opencloud as a dependency -php composer.phar require rackspace/php-opencloud:dev-master -``` - -Once you have installed the library, you will need to load Composer's autoloader (which registers all the required -namespaces): - -```php -require 'vendor/autoload.php'; -``` - -And you're good to go! - -# Quick deep-dive: building some Nova instances - -In this example, you will write code that will create a Nova instance running Ubuntu. - -### 1. Setup the client and pass in your credentials - -To authenticate against Keystone: - -```php -use OpenCloud\OpenStack; - -$client = new OpenStack('http://my-openstack.com:35357/v2.0/', array( - 'username' => 'foo', - 'password' => 'bar', - 'tenantName' => 'baz' -)); -``` - -You will need to substitute in the public URL endpoint for your Keystone service, as well as your `username`, `password` -and `tenantName`. You can also specify your `tenantId` instead of `tenantName` if you prefer. - -### 2. Pick what service you want to use - -In this case, we want to use the Nova service: - -```php -$compute = $client->computeService('nova', 'regionOne'); -``` - -The first argument is the __name__ of the service as it appears in the OpenStack service catalog. If in doubt, you can -leave blank and it will revert to the default name for the service. The second argument is the region. The third and -last argument is the type of URL; you may use either `publicURL` or `internalURL`. - -### 3. Select your server image - -Instances are based on "images", which are effectively just the type of operating system you want. Let's go through the -list and find an Ubuntu one: - -```php -$images = $compute->imageList(); -foreach ($images as $image) { - if (strpos($image->name, 'Ubuntu') !== false) { - $ubuntu = $image; - break; - } -} -``` - -Alternatively, if you already know the image ID, you can do this much easier: - -```php -$ubuntu = $compute->image('868a0966-0553-42fe-b8b3-5cadc0e0b3c5'); -``` - -## 4. Select your flavor - -There are different server specs - some which offer 1GB RAM, others which offer a much higher spec. The 'flavor' of an -instance is its hardware configuration. So if you want a 2GB instance but don't know the ID, you have to traverse the list: - -```php -$flavors = $compute->flavorList(); -foreach ($flavors as $flavor) { - if (strpos($flavor->name, '2GB') !== false) { - $twoGbFlavor = $flavor; - break; - } -} -``` - -Again, it's much easier if you know the ID: - -```php -$twoGbFlavor = $compute->flavor('4'); -``` - -## 5. Thunderbirds are go! - -Okay, you're ready to spin up a server: - -```php -$server = $compute->server(); - -try { - $response = $server->create(array( - 'name' => 'My lovely server', - 'image' => $ubuntu, - 'flavor' => $twoGbFlavor - )); -} catch (\Guzzle\Http\Exception\BadResponseException $e) { - - // No! Something failed. Let's find out: - - $responseBody = (string) $e->getResponse()->getBody(); - $statusCode = $e->getResponse()->getStatusCode(); - $headers = $e->getResponse()->getHeaderLines(); - - echo sprintf("Status: %s\nBody: %s\nHeaders: %s", $statusCode, $responseBody, implode(', ', $headers)); -} -``` - -As you can see, you're creating a server called "My lovely server" - this will take a few minutes for the build to -complete. You can always check the progress by logging into your Controller node and running: - -`nova list` - -You can also execute a polling function immediately after the `create` method that checks the build process: - -```php -use OpenCloud\Compute\Constants\ServerState; - -$callback = function($server) { - if (!empty($server->error)) { - var_dump($server->error); - exit; - } else { - echo sprintf( - "Waiting on %s/%-12s %4s%%", - $server->name(), - $server->status(), - isset($server->progress) ? $server->progress : 0 - ); - } -}; - -$server->waitFor(ServerState::ACTIVE, 600, $callback); -``` -So, the server will be polled until it is in an `ACTIVE` state, with a timeout of 600 seconds. When the poll happens, the -callback function is executed - which in this case just logs some output. - -# More fun with Nova - -Once you've booted up your instance, you can use other API operations to monitor your Compute nodes. To list every -node on record, you can execute: - -```php -$servers = $compute->serverList(); - -foreach ($servers as $server) { - // do something with each server... - echo $server->name, PHP_EOL; -} -``` - -or, if you know a particular instance ID you can retrieve its details: - -```php -$server = $compute->server('xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxx'); -``` - -allowing you to update its properties: - -```php -$server->update(array( - 'name' => 'New server name' -)); -``` - -or delete it entirely: - -```php -$server->delete(); -``` - -# Next steps - -Consult our [documentation](https://github.com/rackspace/php-opencloud/tree/master/docs/userguide) about other services -you can use, like [Keystone](https://github.com/rackspace/php-opencloud/tree/master/docs/userguide/Identity) or -[Swift](https://github.com/rackspace/php-opencloud/tree/master/docs/userguide/ObjectStore). If you have any questions or -troubles, feel free to e-mail sdk-support@rackspace.com or open a Github issue with details. \ No newline at end of file +http://docs.php-opencloud.com/en/latest/getting-started-with-openstack.html diff --git a/rackspace/php-opencloud/docs/getting-started.md b/rackspace/php-opencloud/docs/getting-started.md index 48f99fb86..c7d731a4c 100644 --- a/rackspace/php-opencloud/docs/getting-started.md +++ b/rackspace/php-opencloud/docs/getting-started.md @@ -1,178 +1,5 @@ # Installing the SDK -You must install through Composer, because this library has a few dependencies: +Our docs have moved! Please visit the below link: -```bash -# Install Composer -curl -sS https://getcomposer.org/installer | php - -# Require php-opencloud as a dependency -php composer.phar require rackspace/php-opencloud:dev-master -``` - -Once you have installed the library, you will need to load Composer's autoloader (which registers all the required -namespaces): - -```php -require 'vendor/autoload.php'; -``` - -And you're good to go! - -# Quick deep-dive: building some cloud servers - -In this example, you will write code that will create a Cloud Server running Ubuntu. - -### 1. Setup the client and pass in your credentials - -To authenticate against the Rackspace API and use its services: - -```php - 'foo', - 'apiKey' => 'bar' -)); -``` - -Alternatively, if you would like to validate against your own API, or just want to access OpenStack services: - -```php -use OpenCloud\OpenStack; - -$client = new OpenStack('http://identity.my-openstack.com/v2.0/', array( - 'username' => 'foo', - 'password' => 'bar' -)); -``` - -You can see in the first example that the constant `Rackspace::US_IDENTITY_ENDPOINT` is just a string representation of -Rackspace's identity endpoint (`https://identity.api.rackspacecloud.com/v2.0/`). Another difference is that Rackspace -uses API key for authentication, whereas OpenStack uses a generic password. - -### 2. Pick what service you want to use - -In this case, we want to use the Compute (Nova) service: - -```php -$compute = $client->computeService('cloudServersOpenStack', 'ORD'); -``` - -The first argument is the __name__ of the service as it appears in the OpenStack service catalog. If in doubt, you can -leave blank and it will revert to the default name for the service. The second argument is the region; you may use: - -- __DFW__ (Dallas) -- __ORD__ (Chicago) -- __IAD__ (Virginia) -- __LON__ (London) -- __HKG__ (Hong Kong) -- __SYD__ (Sydney) - -The third and last argument is the type of URL; you may use either `publicURL` or `internalURL`. If you select `internalUrl` -all API traffic will use ServiceNet (internal IPs) and will receive a performance boost. - -### 3. Select your server image - -Servers are based on "images", which are effectively just the type of operating system you want. Let's go through the list -and find an Ubuntu one: - -```php -$images = $compute->imageList(); -while ($image = $images->next()) { - if (strpos($image->name, 'Ubuntu') !== false) { - $ubuntu = $image; - break; - } -} -``` - -Alternatively, if you already know the image ID, you can do this much easier: - -```php -$ubuntu = $compute->image('868a0966-0553-42fe-b8b3-5cadc0e0b3c5'); -``` - -## 4. Select your flavor - -There are different server specs - some which offer 1GB RAM, others which offer a much higher spec. The 'flavor' of a -server is its hardware configuration. So if you want a 2GB instance but don't know the ID, you have to traverse the list: - -```php -$flavors = $compute->flavorList(); -while ($flavor = $flavors->next()) { - if (strpos($flavor->name, '2GB') !== false) { - $twoGbFlavor = $flavor; - break; - } -} -``` - -Again, it's much easier if you know the ID: - -```php -$twoGbFlavor = $compute->flavor('4'); -``` - -## 5. Thunderbirds are go! - -Okay, you're ready to spin up a server: - -```php -use OpenCloud\Compute\Constants\Network; - -$server = $compute->server(); - -try { - $response = $server->create(array( - 'name' => 'My lovely server', - 'image' => $ubuntu, - 'flavor' => $twoGbFlavor, - 'networks' => array( - $compute->network(Network::RAX_PUBLIC), - $compute->network(Network::RAX_PRIVATE) - ) - )); -} catch (\Guzzle\Http\Exception\BadResponseException $e) { - - // No! Something failed. Let's find out: - - $responseBody = (string) $e->getResponse()->getBody(); - $statusCode = $e->getResponse()->getStatusCode(); - $headers = $e->getResponse()->getHeaderLines(); - - echo sprintf("Status: %s\nBody: %s\nHeaders: %s", $statusCode, $responseBody, implode(', ', $headers)); -} -``` - -As you can see, you're creating a server called "My lovely server", and you've inserted it in two networks: the Rackspace -private network (ServiceNet), and the Rackspace public network (for Internet connectivity). This will take a few -minutes for the build to complete. - -You can also call a polling function that checks on the build process: - -```php -use OpenCloud\Compute\Constants\ServerState; - -$callback = function($server) { - if (!empty($server->error)) { - var_dump($server->error); - exit; - } else { - echo sprintf( - "Waiting on %s/%-12s %4s%%", - $server->name(), - $server->status(), - isset($server->progress) ? $server->progress : 0 - ); - } -}; - -$server->waitFor(ServerState::ACTIVE, 600, $callback); -``` -So, the server will be polled until it is in an `ACTIVE` state, with a timeout of 600 seconds. When the poll happens, the -callback function is executed - which in this case just logs some output. \ No newline at end of file +http://docs.php-opencloud.com/en/latest/getting-started-with-rackspace.html diff --git a/rackspace/php-opencloud/docs/userguide/Autoscale/Config.md b/rackspace/php-opencloud/docs/userguide/Autoscale/Config.md old mode 100755 new mode 100644 index a619bf277..328fdcc93 --- a/rackspace/php-opencloud/docs/userguide/Autoscale/Config.md +++ b/rackspace/php-opencloud/docs/userguide/Autoscale/Config.md @@ -1,47 +1,5 @@ # Group configurations -##Intro +Our docs have moved! Please visit the below link: -There are two types of configuration associated with an Auto Scale group: - -- **Group configuration**. Outlines the basic elements of the Auto Scale configuration. The group configuration manages how many servers can participate in the scaling group. It sets a minimum and maximum limit for the number of entities that can be used in the scaling process. It also specifies information related to load balancers. - -- **Launch configuration**. Creates a blueprint for how new servers will be created. The launch configuration specifies what type of server image will be started on launch, what flavor the new server is, and which load balancer the new server connects to. - -## Setup - -To interact with the configuration of a scaling group, you will need to setup the group object beforehand: - -```php -$groupId = 'e41380ae-173c-4b40-848a-25c16d7fa83d'; -$group = $service->getGroup($groupId); -``` - -For more information about setting up the `$service` object, please see the userguide tutorial for [groups](). - -## Get group/launch configuration - -```php -$groupConfig = $group->getGroupConfig(); -$launchConfig = $group->getLaunchConfig(); -``` - -## Edit group/launch configuration - -```php -$groupConfig->update(array( - 'name' => 'New name!' -)); - -$launchConfig = $group->getLaunchConfig(); - -$server = $launchConfig->args->server; -$server->name = "BRAND NEW SERVER NAME"; - -$launchConfig->update(array - 'args' => array( - 'server' => $server, - 'loadBalancers' => $launchConfig->args->loadBalancers - ) -)); -``` \ No newline at end of file +http://docs.php-opencloud.com/en/latest/services/autoscale/group-config.html diff --git a/rackspace/php-opencloud/docs/userguide/Autoscale/Groups.md b/rackspace/php-opencloud/docs/userguide/Autoscale/Groups.md old mode 100755 new mode 100644 index 898d2656f..e0d8bfcdb --- a/rackspace/php-opencloud/docs/userguide/Autoscale/Groups.md +++ b/rackspace/php-opencloud/docs/userguide/Autoscale/Groups.md @@ -1,96 +1,5 @@ #Auto Scale groups -## Intro +Our docs have moved! Please visit the below link: -The scaling group is at the heart of an Auto Scale deployment. The scaling group specifies the basic elements of the Auto Scale configuration. It manages how many servers can participate in the scaling group. It also specifies information related to load balancers if your configuration uses a load balancer. - -## Service setup - -Nothing special here; you setup your client and service objects in the same way as every other resource: - -```php -$service = $client->autoscaleService(); -``` - -Please consult the [client doc](docs/userguide/Client.md) for more information about creating clients. - -## List all groups - -```php -$groups = $service->groupList(); -``` - -Please consult the [iterator doc](docs/userguide/Iterators.md) for more information about iterators. - -## Retrieve one group by ID - -```php -$group = $service->group('605e13f6-1452-4588-b5da-ac6bb468c5bf'); -``` - -## Create a new group - -You can create a new scaling group in two ways: either pass in a JSON string (easier option); or instantiate a new object and manually set its properties (a lot more verbose) - -```php - -// Easy way: - -$jsonString = <<create($jsonString); - -// More granular (and verbose) way: - -$object = new \stdClass; - -// Set the config object for this autoscale group; contains all of properties -// which determine its behaviour -$object->groupConfiguration = new \stdClass; -$object->groupConfiguration->name = 'New autoscale group'; -$object->groupConfiguration->minEntities = 5; -$object->groupConfiguration->maxEntities = 25; -$object->groupConfiguration->cooldown = 60; - -// We need specify what is going to be launched. For now, we'll launch a new server -$object->launchConfiguration = new \stdClass; -$object->launchConfiguration->type = 'launch_server'; - -// To launch a new server, we need two `args` properties: `server` and `loadBalancer` -$server = new \stdClass; -$server->flavorRef = 3; -$server->name = 'webhead'; -$server->imageRef = "0d589460-f177-4b0f-81c1-8ab8903ac7d8"; - -$loadBalancer = new \stdClass; -$loadBalancer->loadBalancerId = 2200; -$loadBalancer->port = 8081; - -$object->launchConfiguration->args = new \stdClass; -$object->launchConfiguration->args->server = $server; -$object->launchConfiguration->args->loadBalancers = array($loadBalancer); - -// Do we want particular scaling policies? -$policy = new \stdClass; -$policy->name = "scale up by 10"; -$policy->change = 10; -$policy->cooldown = 5; -$policy->type = "webhook"; - -$object->scalingPolicies = array($policy); - -$group->create($object); -``` - -## Delete an autoscale group -```php -$group->delete(); -``` - -## Get the current state of the scaling group - -```php -$group->getState(); -``` \ No newline at end of file +http://docs.php-opencloud.com/en/latest/services/autoscale/groups.html diff --git a/rackspace/php-opencloud/docs/userguide/Autoscale/Policies.md b/rackspace/php-opencloud/docs/userguide/Autoscale/Policies.md old mode 100755 new mode 100644 index 6f3d1b282..1f890d7ad --- a/rackspace/php-opencloud/docs/userguide/Autoscale/Policies.md +++ b/rackspace/php-opencloud/docs/userguide/Autoscale/Policies.md @@ -1,68 +1,5 @@ #Policies -## Setup +Our docs have moved! Please visit the below link: -To interact with the policies of a scaling group, you will need to setup the group object beforehand. - -```php -$groupId = 'foobar'; -$group = $service->getGroup($groupId); -``` - -For more information about setting up the `$service` object, please see the userguide tutorial for [Autoscale groups](). - -## Get all policies - -```php -$policies = $group->getPolicies(); - -while ($policy = $policies->next()) { - // do something - echo "{$policy->name} {($policy->type)}" . PHP_EOL; -} -``` - -## Create a new scaling policy - -Creating policies is achieved through passing an array to the `create` method. - -```php -$policy = new \stdClass; -$policy->name = "NEW NAME"; -$policy->change = 1; -$policy->cooldown = 150; -$policy->type = "webhook"; - -$group->getPolicy()->create(array($policy)); - -// or even: - -$group->getPolicy()->create(array( - (object) array( - 'name' => 'NEW NAME', - 'change' => 1, - 'cooldown' => 150, - 'type' => 'webhook' - ), - // etc. -)); -``` - -## Get, update and delete a scaling policy - -```php -$policyId = 'foobar'; -$policy = $group->getPolicy($policyId); - -$policy->update(array( - 'name' => 'More relevant name' -)); - -$policy->delete(); -``` - -## Execute a scaling policy - -```php -$policy->execute(); -``` \ No newline at end of file +http://docs.php-opencloud.com/en/latest/services/autoscale/policies.html diff --git a/rackspace/php-opencloud/docs/userguide/Autoscale/Webhooks.md b/rackspace/php-opencloud/docs/userguide/Autoscale/Webhooks.md old mode 100755 new mode 100644 index e634cc073..783075508 --- a/rackspace/php-opencloud/docs/userguide/Autoscale/Webhooks.md +++ b/rackspace/php-opencloud/docs/userguide/Autoscale/Webhooks.md @@ -1,52 +1,5 @@ # Webhooks -## Setup +Our docs have moved! Please visit the below link: -To interact with the webhooks of a group's scaling policy, you will need to setup the group and policy objects beforehand. - -```php -$groupId = 'foo'; -$policyId = 'bar'; - -$group = $service->getGroup($groupId); -$policy = $group->getPolicy($policyId); -``` - -For more information about setting up the `$service` object, please see the userguide tutorial for [Autoscale groups](). - -## Get all webhooks - -```php -$webhooks = $policy->getWebookList(); -``` - -## Create a new webhook - -```php -$policy->getWebhook()->create(array( - (object) array( - 'name' => 'Alice', - 'metadata' => array( - 'firstKey' => 'foo', - 'secondKey' => 'bar' - ) - ) -)); -``` - -## Get, update and delete an individual webhook - -```php -$webhookId = 'baz'; -$webhook = $policy->getWebhook($webhookId); - -// Update the metadata -$metadata = $webhook->metadata; -$metadata->thirdKey = 'blah'; -$webhook->update(array( - 'metadata' => $metadata -)); - -// Delete it -$webhook->delete(); -``` \ No newline at end of file +http://docs.php-opencloud.com/en/latest/services/autoscale/webhooks.html diff --git a/rackspace/php-opencloud/docs/userguide/Clients.md b/rackspace/php-opencloud/docs/userguide/Clients.md index 954769de7..762a7ed70 100644 --- a/rackspace/php-opencloud/docs/userguide/Clients.md +++ b/rackspace/php-opencloud/docs/userguide/Clients.md @@ -1,99 +1,5 @@ # Clients -## Overview +Our docs have moved! Please visit the below link: -A Client is the object responsibile for issuing HTTP requests and receiving responses from the API. In short, it forms the core of the SDK because it controls how functionality is executed. All services depend on the client to work. - -Users have access to two types of client: `OpenCloud\OpenStack` and `OpenCloud\Rackspace`. The latter extends the former, meaning that much of the functionality is shared between them. The OpenStack client extends functionality from other base classes, that trace all the way back to Guzzle's root class: - -1. `Guzzle\Http\Client` -2. `OpenCloud\Common\Http\Client` -3. `OpenCloud\OpenStack` -4. `OpenCloud\Rackspace` - -## Initializing a client - -### Rackspace - -First, you need to select the Identity endpoint you want to authenticate against. If you're using Rackspace, you can either use the UK or US endpoints. There are class constants defined for your convenience: - -- `OpenCloud\Rackspace::US_IDENTITY_ENDPOINT` (https://identity.api.rackspacecloud.com/v2.0) -- `OpenCloud\Rackspace::UK_IDENTITY_ENDPOINT` (https://lon.identity.api.rackspacecloud.com/v2.0) - -Then you need to find your username and apiKey. Your username will be visible at the top right of the Rackspace Control panel; and your API key can be retrieved by going to Account Settings. Once this is done: - -```php -use OpenCloud\OpenStack; - -$client = new Rackspace(Rackspace::US_IDENTITY_ENDPOINT, array( - 'username' => 'foo', - 'apiKey' => 'bar' -)); -``` - -### OpenStack - -To initialize an OpenStack client, the process is the same: - -```php -use OpenCloud\OpenStack; - -$client = new OpenStack('http://identity.my-openstack.com/v2.0', array( - 'username' => 'foo', - 'password' => 'bar' -)); -``` - -## Authentication - -The Client does not automatically authenticate against the API on object creation - it waits for an API call. When this happens, it checks whether the current "token" has expired, and (re-)authenticates if necessary. - -You can force authentication, by calling: - -```php -$client->authenticate(); -``` - -If the credentials are incorrect, a `401` error will be returned. If credentials are correct, a `200` status is returned with your Service Catalog. - -## Service Catalog - -The Service Catalog is returned on successful authentication, and is composed of all the different API services available to the current tenant. All of this functionality is encapsulated in the `Catalog` object, which allows you greater control and interactivity. - -```php -/** @var OpenCloud\Common\Service\Catalog */ -$catalog = $client->getCatalog(); - -// Return a list of OpenCloud\Common\Service\CatalogItem objects -foreach ($catalog->getItems() as $catalogItem) { - - $name = $catalogItem->getName(); - $type = $catalogItem->getType(); - - if ($name == 'cloudServersOpenStack' && $type == 'compute') { - break; - } - - // Array of OpenCloud\Common\Service\Endpoint objects - $endpoints = $catalogItem->getEndpoints(); - foreach ($endpoints as $endpoint) { - if ($endpoint->getRegion() == 'DFW') { - echo $endpoint->getPublicUrl(); - } - } -} -``` - -As you can see, you have access to each Service's name, type and list of endpoints. Each endpoint provides access to the specific region, along with its public and private endpoint URLs. - -## Default HTTP headers - -To set default HTTP headers: - -```php -$client->setDefaultOption('headers/X-Custom-Header', 'FooBar'); -``` - -## Other functionality - -For a full list of functionality provided by Guzzle, please consult the [official documentation](http://docs.guzzlephp.org/en/latest/http-client/client.html). \ No newline at end of file +http://docs.php-opencloud.com/en/latest/index.html diff --git a/rackspace/php-opencloud/docs/userguide/CloudMonitoring/Agents.md b/rackspace/php-opencloud/docs/userguide/CloudMonitoring/Agents.md old mode 100755 new mode 100644 index 7e67e7d0b..f1b8cab5d --- a/rackspace/php-opencloud/docs/userguide/CloudMonitoring/Agents.md +++ b/rackspace/php-opencloud/docs/userguide/CloudMonitoring/Agents.md @@ -1,149 +1,5 @@ # Agents -## Intro +Our docs have moved! Please visit the below link: -The Monitoring Agent resides on the host server being monitored. The agent allows you to gather on-host metrics based on agent checks and push them to Cloud Monitoring where you can analyze them, use them with the Cloud Monitoring infrastructure (such as alarms), and archive them. - -For more information about this feature, including a brief overview of its core design principles and security layers, see the [official API documentation](http://docs.rackspace.com/cm/api/v1.0/cm-devguide/content/service-agent.html). - -## Setup - -```php -$agentId = '00-agent.example.com'; -$agent = $service->getAgent($agentId); -``` - -You can view the [service page](Service.md) for more information about setting up the Cloud Monitoring service. - -## List agents - -```php -$agents = $service->getAgents(); - -foreach ($agents as $agent) { - echo $agent->getLastConnected(); -} -``` - -Please consult the [iterator doc](docs/userguide/Iterators.md) for more information about iterators. - -## List connections - -```php -$connections = $agent->getConnections(); -``` - -Please consult the [iterator doc](docs/userguide/Iterators.md) for more information about iterators. - -## Get connection -```php -/** @var \OpenCloud\CloudMonitoring\Resource\AgentConnection */ -$connection = $agent->getConnection('cntl4qsIbA'); -``` - -### Agent Connection properties - -Name|Description|Type|Method -----|-----------|----|------ -id|-|-|`getId()` -guid|-|-|`getGuid()` -agent_id|-|-|`getAgentId()` -endpoint|-|-|`getEndpoint()` -process_version|-|-|`getProcessVersion()` -bundle_version|-|-|`getBundleVersion()` -agent_ip|-|-|`getAgentIp()` - - -# Agent tokens - -## Intro - -Agent tokens are used to authenticate Monitoring agents to the Monitoring Service. Multiple agents can share a single token. - -## Setup -```php -$tokenId = '4c5e28f0-0b3f-11e1-860d-c55c4705a286:1234'; -$agentToken = $service->getAgentToken($tokenId); -``` - -## Create agent token -```php -$newToken = $service->getAgentToken(); -$newToken->create(array('label' => 'Foobar')); -``` - -## List agent tokens -```php -$agentTokens = $service->getAgentTokens(); - -foreach ($agentTokens as $token) { - echo $token->getLabel(); -} -``` - -Please consult the [iterator doc](docs/userguide/Iterators.md) for more information about iterators. - -## Update and delete Agent Token -```php -// Update -$token->update(array( - 'label' => 'New label' -)); - -// Delete -$token->delete(); -``` - -# Agent Host Information - -## Info - -An agent can gather host information, such as process lists, network configuration, and memory usage, on demand. You can use the host-information API requests to gather this information for use in dashboards or other utilities. - -## Setup -```php -$host = $monitoringService->getAgentHost(); -``` - -## Get some metrics -```php -$cpuInfo = $host->info('cpus'); -$diskInfo = $host->info('disks'); -$filesystemInfo = $host->info('filesystems'); -$memoryInfo = $host->info('memory'); -$networkIntInfo = $host->info('network_interfaces'); -$processesInfo = $host->info('processes'); -$systemInfo = $host->info('system'); -$userInfo = $host->info('who'); - -// What CPU models do we have? -foreach ($cpuInfo as $cpuMetric) { - echo $cpuMetric->model, PHP_EOL; -} - -// How many disks do we have? -echo $diskInfo->count(); - -// What's the available space on our ext4 filesystem? -foreach ($filesystemInfo as $filesystemMetric) { - if ($filesystemMetric->sys_type_name == 'ext4') { - echo $filesystemMetric->avail; - } -} -``` - -# Agent targets - -## Info - -Each agent check type gathers data for a related set of target devices on the server where the agent is installed. For example, `agent.network` gathers data for network devices. The actual list of target devices is specific to the configuration of the host server. By focusing on specific targets, you can efficiently narrow the metric data that the agent gathers. - -### List agent targets -```php -$targets = $service->getAgentTargets(); - -foreach ($targets as $target) { - echo $target->getType(); -} - -``` \ No newline at end of file +http://docs.php-opencloud.com/en/latest/services/monitoring/agents.html diff --git a/rackspace/php-opencloud/docs/userguide/CloudMonitoring/Alarms.md b/rackspace/php-opencloud/docs/userguide/CloudMonitoring/Alarms.md old mode 100755 new mode 100644 index bf4a75652..b64c1e948 --- a/rackspace/php-opencloud/docs/userguide/CloudMonitoring/Alarms.md +++ b/rackspace/php-opencloud/docs/userguide/CloudMonitoring/Alarms.md @@ -1,56 +1,5 @@ # Alarms -## Info +Our docs have moved! Please visit the below link: -Alarms bind alerting rules, entities, and notification plans into a logical unit. Alarms are responsible for determining a state (```OK```, ```WARNING``` or ```CRITICAL```) based on the result of a Check, and executing a notification plan whenever that state changes. You create alerting rules by using the alarm DSL. For information about using the alarm language, refer to the [reference documentation](http://docs.rackspace.com/cm/api/v1.0/cm-devguide/content/alerts-language.html). - -## Setup - -Alarms are sub-resources of Entities: - -```php -$alarmId = 'alAAAA'; -$alarm = $check->getAlarm(); -``` - -For more information about working with Checks, please see the [appropriate documentation](Checks.md). - -## Attributes - -Name|Description|Required?|Method ----|---|---|--- -check_id|The ID of the check to alert on.|Required|`getCheckId()` -notification_plan_id|The ID of the notification plan to execute when the state changes.|Optional|`getNotificationPlanId()` -criteria|The alarm DSL for describing alerting conditions and their output states.|Optional|`getCriteria()` -disabled|Disable processing and alerts on this alarm|Optional|`isDisabled()` <`bool`> -label|A friendly label for an alarm.|Optional|`getLabel()` -metadata|Arbitrary key/value pairs.|Optional|`getMetadata()` - -## Create Alarm -```php -$alarm->create(array( - 'check_id' => 'chAAAA', - 'criteria' => 'if (metric["duration"] >= 2) { return new AlarmStatus(OK); } return new AlarmStatus(CRITICAL);', - 'notification_plan_id' => 'npAAAAA' -)); -``` - -## List Alarms -```php -$alarms = $entity->getAlarms(); - -foreach ($alarms as $alarm) { - echo $alarm->getId(); -} -``` - -## Update and delete Alarm -```php -// Update -$alarm->update(array( - 'criteria' => 'if (metric["duration"] >= 5) { return new AlarmStatus(OK); } return new AlarmStatus(CRITICAL);' -)); - -// Delete -$alarm->delete(); -``` \ No newline at end of file +http://docs.php-opencloud.com/en/latest/services/monitoring/alarms.html diff --git a/rackspace/php-opencloud/docs/userguide/CloudMonitoring/Changelogs.md b/rackspace/php-opencloud/docs/userguide/CloudMonitoring/Changelogs.md old mode 100755 new mode 100644 index 0f8fe48d0..a1d137c9f --- a/rackspace/php-opencloud/docs/userguide/CloudMonitoring/Changelogs.md +++ b/rackspace/php-opencloud/docs/userguide/CloudMonitoring/Changelogs.md @@ -1,14 +1,5 @@ # Changelogs -## Info +Our docs have moved! Please visit the below link: -The monitoring service records changelogs for alarm statuses. Changelogs are accessible as a Time Series Collection. By default the API queries the last 7 days of changelog information. - -## Working with Changelogs -```php -$changelog = $service->getChangelog(); - -foreach ($changelog as $item) { - $entity = $item->getEntityId(); -} -``` \ No newline at end of file +http://docs.php-opencloud.com/en/latest/services/monitoring/changelogs.html diff --git a/rackspace/php-opencloud/docs/userguide/CloudMonitoring/Checks.md b/rackspace/php-opencloud/docs/userguide/CloudMonitoring/Checks.md old mode 100755 new mode 100644 index 265c0675d..37aefb644 --- a/rackspace/php-opencloud/docs/userguide/CloudMonitoring/Checks.md +++ b/rackspace/php-opencloud/docs/userguide/CloudMonitoring/Checks.md @@ -1,141 +1,5 @@ # Checks -## Info +Our docs have moved! Please visit the below link: -A check is one of the foundational building blocks of the monitoring system. The check determines the parts or pieces of the entity that you want to monitor, the monitoring frequency, how many monitoring zones are originating the check, and so on. When you create a new check in the monitoring system, you specify the following information: - -- A name for the check -- The check's parent entity -- The type of check you're creating -- Details of the check -- The monitoring zones that will launch the check - -The check, as created, will not trigger alert messages until you create an alarm to generate notifications, to enable the creation of a single alarm that acts upon multiple checks (e.g. alert if any of ten different servers stops responding) or multiple alarms off of a single check. (e.g. ensure both that a HTTPS server is responding and that it has a valid certificate). - -## Setup - -Checks are sub-resources of Entities: -```php -$checkId = 'chAAAA'; -$check = $entity->getCheck($checkId); -``` - -## Attributes - -Name|Description|Required?|Data type ----|---|---|--- -type|The type of check.|Required|Valid check type. String (1..25 chars) -details|Details specific to the check type.|Optional|Array -disabled|Disables the check.|Optional|Boolean -label|A friendly label for a check.|Optional|String (1..255 chars) -metadata|Arbitrary key/value pairs.|Optional|Array -period|The period in seconds for a check. The value must be greater than the minimum period set on your account.|Optional|Integer (30..1800) -timeout|The timeout in seconds for a check. This has to be less than the period.|Optional|Integer (2..1800) - -###Attributes used for remote Checks - -Name|Description|Required?|Data type ----|---|---|--- -monitoring_zones_poll|List of monitoring zones to poll from. Note: This argument is only required for remote (non-agent) checks|Optional|Array -target_alias|A key in the entity's `ip_addresses` hash used to resolve this check to an IP address. This parameter is mutually exclusive with target_hostname.|Optional|String (1..64 chars) -target_hostname|The hostname this check should target. This parameter is mutually exclusive with `target_alias`.|Optional|Valid FQDN, IPv4 or IPv6 address. String (1..256 chars). -target_resolver|Determines how to resolve the check target.|Optional|`IPv4` or `IPv6` - -## Test parameters (before create) -```php -$params = array( - 'type' => 'remote.http', - 'details' => array( - 'url' => 'http://example.com', - 'method' => 'GET' - ), - 'monitoring_zones_poll' => array('mzlon'), - 'period' => '100', - 'timeout' => '30', - 'target_alias' => 'default', - 'label' => 'Website check 1' -); - -// You can do a test to see what would happen -// if a Check is launched with these params -$response = $entity->testNewCheckParams($params); - -echo $response->timestamp; // When was it executed? -echo $response->available; // Was it available? -echo $response->status; // Status code -``` -## Create a Check -```php -$entity->createCheck($params); -``` - -## Test existing Check -```php -// Set arg to TRUE for debug information -$response = $check->test(true); - -echo $response->debug_info; -``` - -## List Checks -```php -$checks = $entity->getChecks(); - -foreach ($checks as $check) { - echo $check->getId(); -} -``` - -### Update and delete Check -```php -// Update -$check->update(array('period' => 500)); - -// Delete -$check->delete(); -``` - -# Check types - -## Info - -Each check within the Rackspace Cloud Monitoring has a designated check type. The check type instructs the monitoring system how to check the monitored resource. **Note:** Users cannot create, update or delete check types. - -Check types for commonly encountered web protocols, such as HTTP (```remote.http```), IMAP (```remote.imap-banner```) , SMTP (```remote.stmp```), and DNS (```remote.dns```) are provided. Monitoring commonly encountered infrastructure servers like MySQL (```remote.mysql-banner```) and PostgreSQL (```remote.postgresql-banner```) are also available. Monitoring custom server uptime can be accomplished with the remote.tcp banner check to check for a protocol-defined banner at the beginning of a connection. Gathering metrics from server software to create alerts against can be accomplished using the remote.http check type and the 'extract' attribute to define the format. - -In addition to the standard Cloud Monitoring check types, you can also use agent check types if the Monitoring Agent is installed on the server you are monitoring. For a list of available check types, see the [official API documentation](http://docs.rackspace.com/cm/api/v1.0/cm-devguide/content/appendix-check-types.html). - -Checks generate metrics that alarms will alert based upon. The metrics generated often times depend on the check's parameters. For example, using the 'extract' attribute on the remote.http check, however the default metrics will always be present. To determine the exact metrics available, the Test Check API is provided. - -## Setup - -If you want to see the type for an existing Check: - -```php -/** @var \OpenCloud\CloudMonitoring\Resource\CheckType */ -$checkType = $check->getCheckType(); -``` - -Alternatively, you can retrieve a specific type based on its ID: - -```php -$checkTypeId = 'remote.dns'; -$checkType = $service->getCheckType($checkTypeId); -``` - -## Attributes - -Name|Description|Data type|Method ----|---|---|--- -type|The name of the supported check type.|String|`getType()` -fields|Check type fields.|Array|`getFields()` -supported_platforms|Platforms on which an agent check type is supported. This is advisory information only - the check may still work on other platforms, or report that check execution failed at runtime|Array|`getSupportedPlatforms()` - -## List all possible check types -```php -$checkTypes = $service->getCheckTypes(); - -foreach ($checkTypes as $checkType) { - echo $checkType->getId(); -} -``` \ No newline at end of file +http://docs.php-opencloud.com/en/latest/services/monitoring/checks.html diff --git a/rackspace/php-opencloud/docs/userguide/CloudMonitoring/Entities.md b/rackspace/php-opencloud/docs/userguide/CloudMonitoring/Entities.md old mode 100755 new mode 100644 index c9885604b..0f09a74a2 --- a/rackspace/php-opencloud/docs/userguide/CloudMonitoring/Entities.md +++ b/rackspace/php-opencloud/docs/userguide/CloudMonitoring/Entities.md @@ -1,55 +1,5 @@ # Entities -## Info +Our docs have moved! Please visit the below link: -An entity is the target of what you are monitoring. For example, you can create an entity to monitor your website, a particular web service, or your Rackspace server. Note that an entity represents only one item in the monitoring system -- if you wanted to monitor each server in a cluster, you would create an entity for each of the servers. You would not create a single entity to represent the entire cluster. - -An entity can have multiple checks associated with it. This allows you to check multiple services on the same host by creating multiple checks on the same entity, instead of multiple entities each with a single check. - -## Setup - -```php -$entity = $service->getEntity(); -``` - -For more information about setting up the `$service` object, please see the userguide tutorial for [services](Service.md). - -## Attributes - -Name|Description|Required?|Data type|Method ----|---|---|---|--- -label|Defines a name for the entity.|Required|String (1..255 chars)|`getLabel()` -agent_id|Agent to which this entity is bound to.|Optional|String matching the regex: `/^[-\.\w]{1,255}$/`|`getAgentId()` -ip_addresses|Hash of IP addresses that can be referenced by checks on this entity.|Optional|Array|`getIpAddresses()` -metadata|Arbitrary key/value pairs that are passed during the alerting phase.|Optional|`OpenCloud\Common\Metadata`|`getMetadata()` - -## Create Entity -```php -$service->createEntity(array( - 'label' => 'Brand New Entity', - 'ip_addresses' => array( - 'default' => '127.0.0.4', - 'b' => '127.0.0.5', - 'c' => '127.0.0.6', - 'test' => '127.0.0.7' - ), - 'metadata' => array( - 'all' => 'kinds', - 'of' => 'stuff', - 'can' => 'go', - 'here' => 'null is not a valid value' - ) -)); -``` - - -## Update and delete Entity -```php -// Update -$entity->update(array( - 'label' => 'New label for my entity' -)); - -// Delete -$entity->delete(); -``` \ No newline at end of file +http://docs.php-opencloud.com/en/latest/services/monitoring/entities.html diff --git a/rackspace/php-opencloud/docs/userguide/CloudMonitoring/Metrics.md b/rackspace/php-opencloud/docs/userguide/CloudMonitoring/Metrics.md old mode 100755 new mode 100644 index 6799f8881..86450bee9 --- a/rackspace/php-opencloud/docs/userguide/CloudMonitoring/Metrics.md +++ b/rackspace/php-opencloud/docs/userguide/CloudMonitoring/Metrics.md @@ -1,79 +1,5 @@ # Metrics -## Info +Our docs have moved! Please visit the below link: -When Monitoring checks run, they generate metrics. These metrics are stored as full resolution data points in the Cloud Monitoring system. Full resolution data points are periodically rolled up (condensed) into coarser data points. - -Depending on your needs, you can use the metrics API to fetch individual data points (fine-grained) or rolled up data points (coarse-grained) over a period of time. - -## Data Granularity - -Cloud Monitoring supports several granularities of data: full resolution data and rollups computed at 5, 20, 60, 240 and 1440 minute intervals. - -When you fetch metrics data points, you specify several parameters to control the granularity of data returned: - -- A time range for the points -- Either the number of points you want returned OR the resolution of the data you want returned - -When you query by points, the API selects the resolution that will return you the number of points you requested. The API makes the assumption of a 30 second frequency, performs the calculation, and selects the appropriate resolution. - -**Note:** Because the API performs calculations to determine the points returned for a particular resolution, the number of points returned may differ from the specific number of points you request. - -Consider that you want to query data for a 48-hour time range between the timestamps `from=1354647221000` and `to=1358794421000` ( **specified in Unix time, based on the number of milliseconds that have elapsed since January 1, 1970** ). The following table shows the number of points that the API returns for a given resolution. - -#### Specifying resolution to retrieve data in 48 hour period - -You specify resolution...|API returns points... ----|--- -FULL|5760 -MIN5|576 -MIN20|144 -MIN60|48 -MIN240|12 -MIN1440|2 - -#### Specifying number of points to retrieve data in 48 hour period - -You specify points in the range...|API calculates resolution ----|--- -3168-∞|FULL -360-3167|MIN5 -96-359|MIN20 -30-95|MIN60 -7-29|MIN240 -0-6|MIN1440 - -#### Data Point Expiration - -Cloud Monitoring expires data points according to the following schedule: - -Resolution|Expiration ----|--- -FULL|2 days -MIN5|7 days -MIN20|15 days -MIN60|30 days -MIN240|60 days -MIN1440|365 days - -## Setup - -Metrics are sub-resources of Checks. For more information about working with Checks, please see the [relevant documentation](Checks.md). - -## List all metrics -```php -$metrics = $check->getMetrics(); - -foreach ($metrics as $metric) { - echo $metric->getName(); -} -``` - -## Fetch data points -```php -$data = $check->fetchDataPoints('mzdfw.available', array( - 'resolution' => 'FULL', - 'from' => 1369756378450, - 'to' => 1369760279018 -)); -``` \ No newline at end of file +http://docs.php-opencloud.com/en/latest/services/monitoring/metrics.html diff --git a/rackspace/php-opencloud/docs/userguide/CloudMonitoring/Notifications.md b/rackspace/php-opencloud/docs/userguide/CloudMonitoring/Notifications.md old mode 100755 new mode 100644 index 81a890f56..30a1cf97c --- a/rackspace/php-opencloud/docs/userguide/CloudMonitoring/Notifications.md +++ b/rackspace/php-opencloud/docs/userguide/CloudMonitoring/Notifications.md @@ -1,203 +1,5 @@ # Notifications -## Info +Our docs have moved! Please visit the below link: -A notification is a destination to send an alarm; it can be a variety of different types, and will evolve over time. - -For instance, with a webhook type notification, Cloud Monitoring posts JSON formatted data to a user-specified URL on an alert condition (Check goes from `OK` -> `CRITICAL` and so on). - -## Setup - -```php -$id = 'ntAAAA'; -$notification = $service->getNotification($id); -``` - -## Attributes - -Name|Description|Data type|Method ----|---|---|---|--- -details|A hash of notification specific details based on the notification type.|Array|`getDetails()` -label|Friendly name for the notification.|String (1..255 chars)|`getLabel()` -type|The notification type to send.|String. Either `webhook`, `email`, or `pagerduty`|`getType()` - -## Test parameters -```php -$params = array( - 'label' => 'My webhook #1', - 'type' => 'webhook', - 'details' => array( - 'url' => 'http://example.com' - ) -); - -// Test it -$response = $notification->testParams($params); - -if ($response->status == 'Success') { - echo $response->message; -} -``` - -## Create Notification - -```php -$notification->create($params); -``` - -## Test existing notification -```php -$response = $notification->testExisting(true); -echo $response->debug_info; -``` - -## List Notifications -```php -$notifications = $service->getNotifications(); - -foreach ($notifications as $notification) { - echo $notification->getId(); -} -``` - -## Update and delete Notifications -```php -// Update -$notification->update(array( - 'label' => 'New notification label' -)); - -// Delete -$notification->delete(); -``` - -# Notification types - -## Info - -Pretty self-explanatory. Rackspace Cloud Monitoring currently supports the following notification types: - -#### Webhook - -Industry-standard web hooks, where JSON is posted to a configurable URL. It has these attributes: - -Name|Description|Data type ----|---|--- -address|Email address to send notifications to|Valid email - -#### Email - -Email alerts where the message is delivered to a specified address. It has these attributes: - -Name|Description|Data type ----|---|--- -url|An HTTP or HTTPS URL to POST to|Valid URL - -## Setup - -If you've already set up a main Notification object, and want to access functionality for this Notification's particular Notification Type, you can access its property: - -```php -$type = $notification->getNotificationType(); -``` - -Alternatively, you can retrieve an independent resource using the ID: - -```php -$typeId = 'pagerduty'; -$type = $service->getNotificationType($typeId); -``` - -## List all possible notification types -```php -$types = $service->getNotificationTypes(); - -foreach ($types as $type) { - echo sprintf('%s %s', $type->getName(), $type->getDescription()); -} -``` - -# Notification plans - -## Info - -A notification plan contains a set of notification actions that Rackspace Cloud Monitoring executes when triggered by an alarm. Rackspace Cloud Monitoring currently supports webhook and email notifications. - -Each notification state can contain multiple notification actions. For example, you can create a notification plan that hits a webhook/email to notify your operations team if a warning occurs. However, if the warning escalates to an Error, the notification plan could be configured to hit a different webhook/email that triggers both email and SMS messages to the operations team. The notification plan supports the following states: - -- Critical -- Warning -- OK - -A notification plan, `npTechnicalContactsEmail`, is provided by default which will email all of the technical contacts on file for an account whenever there is a state change. - -## Setup - -```php -$planId = 'npAAAA'; -$plan = $service->getNotificationPlan(); -``` - -## Attributes - -Name|Description|Required?|Data type|Method ----|---|---|---|--- -label|Friendly name for the notification plan.|Required|String (1..255 chars)|`getLabel()` -critical_state|The notification list to send to when the state is `CRITICAL`.|Optional|Array|`getCriticalState()` -ok_state|The notification list to send to when the state is `OK`.|Optional|Array|`getOkState()` -warning_state|The notification list to send to when the state is `WARNING`.|Optional|Array|`getWarningState()` - -## Create Notification Plan -```php -$plan->create(array( - 'label' => 'New Notification Plan', - 'critical_state' => array('ntAAAA'), - 'ok_state' => array('ntBBBB'), - 'warning_state' => array('ntCCCC') -)); -``` - -## Update and delete Notification Plan - -```php -// Update -$plan->update(array( - 'label' => 'New label for my plan' -)); - -// Delete -$plan->delete(); -``` - -# Alarm Notification History - -## Info - -The monitoring service keeps a record of notifications sent for each alarm. This history is further subdivided by the check on which the notification occurred. Every attempt to send a notification is recorded, making this history a valuable tool in diagnosing issues with unreceived notifications, in addition to offering a means of viewing the history of an alarm's statuses. - -Alarm notification history is accessible as a Time Series Collection. By default alarm notification history is stored for 30 days and the API queries the last 7 days of information. - -## Setup - -Notification History is a sub-resource of an Alarm. For more information about working with Alarms, please consult the relevant [documentation](Alarms.md). - -## Discover which Checks have a Notification History - -This operation list checks for which alarm notification history is available: - -```php -$checks = $alarm->getRecordedChecks(); -``` - -## List Alarm Notification History for a particular Check -```php -$checkHistory = $alarm->getNotificationHistoryForCheck('chAAAA'); -``` - -## Get a particular Notification History item -```php -$checkId = 'chAAAA'; -$itemUuid = '646ac7b0-0b34-11e1-a0a1-0ff89fa2fa26'; - -$singleItem = $history->getNotificationHistoryItem($checkId, $itemUuid); -``` \ No newline at end of file +http://docs.php-opencloud.com/en/latest/services/monitoring/notifications.html diff --git a/rackspace/php-opencloud/docs/userguide/CloudMonitoring/Service.md b/rackspace/php-opencloud/docs/userguide/CloudMonitoring/Service.md old mode 100755 new mode 100644 index 5647eedc2..be9f9435f --- a/rackspace/php-opencloud/docs/userguide/CloudMonitoring/Service.md +++ b/rackspace/php-opencloud/docs/userguide/CloudMonitoring/Service.md @@ -1,16 +1,5 @@ # Cloud Monitoring Service -Initializing the Cloud Monitoring is easy - and can be done in a similar way to all other Rackspace services: +Our docs have moved! Please visit the below link: -1. Create client and pass in auth details. For more information about creating clients, please consult the [Client documentation](../Clients.md). -2. Use the factory method, specifying additional parameters where necessary: - -```php -$service = $client->cloudMonitoringService('cloudMonitoring', 'ORD', 'publicURL'); -``` - -All three parameters are optional - if not specified, it will revert to the service's default values which are: - -- Name = `cloudMonitoring` -- Region = `DFW` -- URL type = `publicURL` \ No newline at end of file +http://docs.php-opencloud.com/en/latest/services/monitoring/index.html diff --git a/rackspace/php-opencloud/docs/userguide/CloudMonitoring/Views.md b/rackspace/php-opencloud/docs/userguide/CloudMonitoring/Views.md old mode 100755 new mode 100644 index b6b505efc..7672c2ea8 --- a/rackspace/php-opencloud/docs/userguide/CloudMonitoring/Views.md +++ b/rackspace/php-opencloud/docs/userguide/CloudMonitoring/Views.md @@ -1,19 +1,5 @@ # Views -## Info +Our docs have moved! Please visit the below link: -Views contain a combination of data that usually includes multiple, different objects. The primary purpose of a view is to save API calls and make data retrieval more efficient. Instead of doing multiple API calls and then combining the result yourself, you can perform a single API call against the view endpoint. - -## List all Views - -```php -$views = $service->getViews(); - -foreach ($views as $view) { - $entity = $view->getEntity(); - - echo $view->getTimestamp(); -} - ``` - -Please consult the [iterator doc](docs/userguide/Iterators.md) for more information about iterators. \ No newline at end of file +http://docs.php-opencloud.com/en/latest/services/monitoring/views.html diff --git a/rackspace/php-opencloud/docs/userguide/CloudMonitoring/Zones.md b/rackspace/php-opencloud/docs/userguide/CloudMonitoring/Zones.md old mode 100755 new mode 100644 index 338bb1053..8257efa29 --- a/rackspace/php-opencloud/docs/userguide/CloudMonitoring/Zones.md +++ b/rackspace/php-opencloud/docs/userguide/CloudMonitoring/Zones.md @@ -1,46 +1,5 @@ # Zones -## Info +Our docs have moved! Please visit the below link: -A monitoring zone is a location that Rackspace Cloud Monitoring collects data from. Examples of monitoring zones are "US West", "DFW1" or "ORD1". It is an abstraction for a general location from which data is collected. - -An "endpoint," also known as a "collector," collects data from the monitoring zone. The endpoint is mapped directly to an individual machine or a virtual machine. A monitoring zone contains many endpoints, all of which will be within the IP address range listed in the response. The opposite is not true, however, as there may be unallocated IP addresses or unrelated machines within that IP address range. - -A check references a list of monitoring zones it should be run from. - -## Setup -```php -$zoneId = 'mzAAAAA'; -$zone = $monitoringService->getMonitoringZone($zoneId); -``` - -## Attributes - -Name|Description|Data type|Method ----|---|---|---|--- -country_code|Country Code|String longer than 2 characters|`getCountryCode()` -label|Label|String|`getLabel()` -source_ips|Source IP list|Array|`getSourceIps()` - -## List all zones - -```php -$zones = $service->getMonitoringZones(); -``` - -Please consult the [iterator doc](docs/userguide/Iterators.md) for more information about iterators. - -## Perform a traceroute - -```php -$traceroute = $zone->traceroute(array( - 'target' => 'http://test.com', - 'target_resolver' => 'IPv4' -)); - -// How many hops? -echo count($traceroute); - -// What was the first hop's IP? -echo $traceroute[0]->ip; -``` \ No newline at end of file +http://docs.php-opencloud.com/en/latest/services/monitoring/zones.html diff --git a/rackspace/php-opencloud/docs/userguide/Compute/Images.md b/rackspace/php-opencloud/docs/userguide/Compute/Images.md index d09b0b0eb..b7a99025d 100644 --- a/rackspace/php-opencloud/docs/userguide/Compute/Images.md +++ b/rackspace/php-opencloud/docs/userguide/Compute/Images.md @@ -1,65 +1,5 @@ # Compute Images -## Intro +Our docs have moved! Please visit the below link: -An image is a collection of files for a specific operating system that you use to create or rebuild a server. Rackspace provides prebuilt images. You can also create custom images from servers that you have launched. - -In addition to creating images manually, you can also schedule images of your server automatically. Please consult the [official docs](http://docs.rackspace.com/servers/api/v2/cs-devguide/content/scheduled_images.html) for more information about this extension, including enabling and disabling scheduled images and showing scheduled images. - -With standard servers, the entire disk (OS and data) is captured in the image. With Performance servers, only the system disk is captured in the image. The data disks should be backed up using Cloud Backup or Cloud Block Storage to ensure availability in case you need to rebuild or restore a server. - -## Setup - -You first need to setup a Compute service. For information, please consult the [Compute service](Service.md) documentation. - -## List images - -```php -$images = $service->imageList(); - -foreach ($images as $image) { - -} -``` - -For more information about [iterators](docs/userguide/Iterators.md), please consult the official documentation. - -### Query parameters - -You can also refine the list of images returned by providing specific URL parameters: - -|Field name|Description| -|---|---| -|server|Filters the list of images by server. Specify the server reference by ID or by full URL.| -|name|Filters the list of images by image name.| -|status|Filters the list of images by status. In-flight images have a status of `SAVING` and the conditional progress element contains a value from 0 to 100, which indicates the percentage completion. For a full list, please consult the `OpenCloud\Compute\Constants\ImageState` class. Images with an `ACTIVE` status are available for use.| -|changes-since|Filters the list of images to those that have changed since the changes-since time. See the [official docs](http://docs.rackspace.com/servers/api/v2/cs-devguide/content/ChangesSince.html) for more information.| -|marker|The ID of the last item in the previous list. See the [official docs](http://docs.rackspace.com/servers/api/v2/cs-devguide/content/Paginated_Collections-d1e664.html) for more information.| -|limit|Sets the page size. See the [official docs](http://docs.rackspace.com/servers/api/v2/cs-devguide/content/Paginated_Collections-d1e664.html) for more information.| -|type|Filters base Rackspace images or any custom server images that you have created. Can either be `BASE` or `SNAPSHOT`.| - -### Example - -You can return more information about each image by setting the `$details` argument to `true`. The second argument can be an array of query parameters: - -```php -use OpenCloud\Compute\Constants\ImageState; - -$list = $service->imageList(true, array( - 'server' => 'fooBar', - 'status' => ImageState::ACTIVE -)); -``` - -## Get an image - -```php -$imageId = '3afe97b2-26dc-49c5-a2cc-a2fc8d80c001'; -$image = $service->image($imageId); -``` - -## Delete an image - -```php -$image->delete(); -``` \ No newline at end of file +http://docs.php-opencloud.com/en/latest/services/compute/images.html diff --git a/rackspace/php-opencloud/docs/userguide/Compute/Keypair.md b/rackspace/php-opencloud/docs/userguide/Compute/Keypair.md index c5ed90073..a3e0b4870 100644 --- a/rackspace/php-opencloud/docs/userguide/Compute/Keypair.md +++ b/rackspace/php-opencloud/docs/userguide/Compute/Keypair.md @@ -1,84 +1,5 @@ # Keypairs -## Generate new keypair +Our docs have moved! Please visit the below link: -This operation creates a new keypair under a provided name; the public key value is automatically generated for you. - -```php -$keypair = $service->keypair(); - -$keypair->create(array( - 'name' => 'jamie_keypair_1' -)); - -echo $keypair->getPublicKey(); -``` - -## Upload existing keypair - -This operation creates a new keypair under a provided name using a provided public key value. This public key will probably exist on your local filesystem, and so provide easy access to your server when uploaded. - -```php -$keypair = $service->keypair(); - -$key = <<create(array( - 'name' => 'jamie_macbook', - 'publicKey' => $key -)); - -``` - -## List keypairs - -To list all existing keypairs: - -```php -$keys = $service->listKeypairs(); - -foreach ($keys as $key) { - // ... -} -``` - -For more information about iterators, please see [the docs](../Iterators.md). - -## Delete keypairs - -To delete a specific keypair: - -```php -$keypair->delete(); -``` - -## Creating a server with a keypair - -In order to spawn an instance with a saved keypair (allowing you to SSH in without passwords), you create your server -using the same operation as usual, with one extra parameter: - -```php -use Guzzle\Http\Exception\BadResponseException; -use OpenCloud\Compute\Constants\Network; - -$server = $compute->server(); - -try { - $response = $server->create(array( - 'name' => 'New server', - 'image' => $ubuntuImage, - 'flavor' => $twoGbFlavor, - 'networks' => array( - $compute->network(Network::RAX_PUBLIC), - $compute->network(Network::RAX_PRIVATE) - ), - 'keypair' => 'jamie_macbook' - )); -} catch (BadResponseException $e) { - // error... -} -``` - -So, as you can see, you specify the **name** of an existing keypair that you previously created on the API. \ No newline at end of file +http://docs.php-opencloud.com/en/latest/services/compute/keypairs.html diff --git a/rackspace/php-opencloud/docs/userguide/Compute/Server.md b/rackspace/php-opencloud/docs/userguide/Compute/Server.md index e141fa8d1..dedc1da77 100644 --- a/rackspace/php-opencloud/docs/userguide/Compute/Server.md +++ b/rackspace/php-opencloud/docs/userguide/Compute/Server.md @@ -1,143 +1,5 @@ # Servers -## Intro +Our docs have moved! Please visit the below link: -A server is a virtual machine instance in the Cloud Servers environment. - -## Setup - -Server objects are instantiated from the Compute service. For more details, see the [Service](Service.md) docs. - -## Get server - -The easiest way to retrieve a specific server is by its unique ID: - -```php -$serverId = 'ef08aa7a-b5e4-4bb8-86df-5ac56230f841'; -$server = $service->server($serverId); -``` - -## List servers - -You can list servers in two different ways: - -* return an _overview_ of each server (ID, name and links) -* return _detailed information_ for each server - -Knowing which option to use might help save unnecessary bandwidth and reduce latency. - -```php -// overview -$servers = $service->serverList(); - -// detailed -$servers = $service->serverList(true); -``` - -### URL parameters for filtering servers - -Name|Description|Type ----|---|--- -image|The image ID|string -flavor|The flavor ID|string -name|The server name|string -status|The server status. Servers contain a status attribute that indicates the current server state. You can filter on the server status when you complete a list servers request, and the server status is returned in the response body. For a full list, please consult `OpenCloud\Compute\Constants\ServerState`|string -changes-since|Value for checking for changes since a previous request|A valid ISO 8601 dateTime (2011-01-24T17:08Z) -RAX-SI:image_schedule|If scheduled images enabled or not. If the value is TRUE, the list contains all servers that have an image schedule resource set on them. If the value is set to FALSE, the list contains all servers that do not have an image schedule.|bool - -## Create server - -There are a few parameter requirements when creating a server: - -* **name** - needs to be a string; -- **flavor** - a `OpenCloud\Compute\Resource\Flavor` object, that is populated with the values of a real API flavor; -* **image** - a `OpenCloud\Compute\Resource\Image` object, that is populated with the values of a real API image; -* **networks** - an array of `OpenCloud\Compute\Resource\Network` objects which represent which networks you compute instance will be placed in. - -Firstly we need to find our flavor and image using their UUIDs. For more information about these concepts, including how to find flavor/image UUIDs, please consult §§ 3-4 in the [Getting Started guide](https://github.com/rackspace/php-opencloud/blob/master/docs/getting-started.md#3-select-your-server-image). - -```php -$ubuntuImage = $compute->image('868a0966-0553-42fe-b8b3-5cadc0e0b3c5'); -$twoGbFlavor = $compute->flavor('4'); -``` - -Now we're ready to create our instance: - -```php -use OpenCloud\Compute\Constants\Network; - -$server = $compute->server(); - -try { - $response = $server->create(array( - 'name' => 'My lovely server', - 'image' => $ubuntuImage, - 'flavor' => $twoGbFlavor, - 'networks' => array( - $compute->network(Network::RAX_PUBLIC), - $compute->network(Network::RAX_PRIVATE) - ) - )); -} catch (\Guzzle\Http\Exception\BadResponseException $e) { - - // No! Something failed. Let's find out: - $responseBody = (string) $e->getResponse()->getBody(); - $statusCode = $e->getResponse()->getStatusCode(); - $headers = $e->getResponse()->getHeaderLines(); - - echo sprintf('Status: %s\nBody: %s\nHeaders: %s', $statusCode, $responseBody, implode(', ', $headers); -} -``` - -It's always best to be defensive when executing functionality over HTTP; you can achieve this best by wrapping calls in a try/catch block. It allows you to debug your failed operations in a graceful and efficient manner. - -### Create parameters - -Name|Description|Type|Required ----|---|---|--- -name|The server name. The name that you specify in a create request becomes the initial host name of the server. After the server is built, if you change the server name in the API or change the host name directly, the names are not kept in sync.|string|Yes -flavor|A populated `OpenCloud\Compute\Resource\Flavor` object representing your chosen flavor|object|Yes -image|A populated `OpenCloud\Compute\Resource\Image` object representing your chosen image|object|Yes -OS-DCF:diskConfig|The disk configuration value. You can use two options: `AUTO` or `MANUAL`.

    `AUTO` means the server is built with a single partition the size of the target flavor disk. The file system is automatically adjusted to fit the entire partition. This keeps things simple and automated. `AUTO` is valid only for images and servers with a single partition that use the EXT3 file system. This is the default setting for applicable Rackspace base images.

    `MANUAL` means the server is built using whatever partition scheme and file system is in the source image. If the target flavor disk is larger, the remaining disk space is left unpartitioned. This enables images to have non-EXT3 file systems, multiple partitions, and so on, and enables you to manage the disk configuration.|string|No -networks|An array of populated `OpenCloud\Compute\Resource\Network` objects that indicate which networks your instance resides in.|array|Yes -metadata|An array of arbitrary data (key-value pairs) that adds additional meaning to your server.|array|No -keypair|You can install a registered keypair onto your newly created instance, thereby providing scope for keypair-based authentication.|array|No -personality|Files that you can upload to your newly created instance's filesystem.|array|No - -### Creating a server with keypairs - -Please see the [Keypair](Keypair.md) docs for more information. - -### Creating a server with personality files - -Before you execute the create operation, you can add "personality" files to your `OpenCloud\Compute\Resource\Server` object. These files are structured as a flat array. - -```php -$server->addFile('/var/test_file', 'FILE CONTENT'); -``` - -As you can see, the first parameter represents the filename, and the second is a string representation of its content. When the server is created these files will be created on its local filesystem. For more information about server personality files, please consult the [official documentation](http://docs.rackspace.com/servers/api/v2/cs-devguide/content/Server_Personality-d1e2543.html). - -## Update server - -You can update certain attributes of an existing server instance. These attributes are detailed in the next section. - -```php -$server->update(array( - 'name' => 'NEW SERVER NAME' -)); -``` - -### Updatable attributes - -name|description ----|--- -name|The name of the server. If you edit the server name, the server host name does not change. Also, server names are not guaranteed to be unique. -accessIPv4|The IP version 4 address. -accessIPv6|The IP version 6 address. - -## Delete server - -```php -$server->delete(); -``` \ No newline at end of file +http://docs.php-opencloud.com/en/latest/services/compute/servers.html diff --git a/rackspace/php-opencloud/docs/userguide/Compute/Service.md b/rackspace/php-opencloud/docs/userguide/Compute/Service.md index faff788bb..9bb8e996a 100644 --- a/rackspace/php-opencloud/docs/userguide/Compute/Service.md +++ b/rackspace/php-opencloud/docs/userguide/Compute/Service.md @@ -1,18 +1,5 @@ # Compute service -## Setup +Our docs have moved! Please visit the below link: -To instantiate a Compute service object, you first need to setup a Rackspace/OpenStack client. To do this, or for more -information, please consult the [Clients documentation](../Clients.md). - -```php -$service = $client->computeService(); -``` - -If no arguments are provided to the above method, certain values are set to their default values: - -|Param|Default value| -|---|---| -|`$name`|cloudServersOpenStack| -|`$region`|DFW| -|`$urltype`|publicURL| \ No newline at end of file +http://docs.php-opencloud.com/en/latest/services/compute/index.html diff --git a/rackspace/php-opencloud/docs/userguide/Identity/Roles.md b/rackspace/php-opencloud/docs/userguide/Identity/Roles.md index e2d49e71d..5d608bf51 100644 --- a/rackspace/php-opencloud/docs/userguide/Identity/Roles.md +++ b/rackspace/php-opencloud/docs/userguide/Identity/Roles.md @@ -1,68 +1,5 @@ # Roles -## Intro +Our docs have moved! Please visit the below link: -A role is a personality that a user assumes when performing a specific set of operations. A role includes a set of rights and privileges. A user assuming a role inherits the rights and privileges associated with the role. A token that is issued to a user includes the list of roles the user can assume. When a user calls a service, that service determines how to interpret a user's roles. A role that grants access to a list of operations or resources within one service may grant access to a completely different list when interpreted by a different service. - -## Setup - -Role objects are instantiated from the Identity service. For more details, see the [Service](Service.md) docs. - -## Useful object properties/methods - -Property|Getter|Setter ----|---|--- -id|`getId()`|`setId()` -name|`getName()`|`setName()` -description|`getDescription()`|`setDescription()` - -## List roles - -This call lists the global roles available within a specified service. - -```php -$roles = $service->getRoles(); - -foreach ($roles as $role) { - // ... -} -``` - -For more information about how to use iterators, see the [documentation](../Iterators.md). - -## Get role - -This call lists detailed information (id, name, description) for a specified role. - -```php -$roleId = '123abc'; -$role = $service->getRole($roleId); -``` - -## Add/delete user roles - -To add/remove user roles, you must first instantiate a [user](Users.md) object: - -```php -$roleId = '123abc'; - -// add role to user -$user->addRole($roleId); - -// remove role from user -$user->removeRole($roleId); -``` - -## List user global roles - -This call returns a list of global roles associated with a user: - -```php -$roles = $user->getRoles(); - -foreach ($roles as $role) { - // ... -} -``` - -For more information about how to use iterators, see the [documentation](../Iterators.md). \ No newline at end of file +http://docs.php-opencloud.com/en/latest/services/identity/roles.html diff --git a/rackspace/php-opencloud/docs/userguide/Identity/Service.md b/rackspace/php-opencloud/docs/userguide/Identity/Service.md index 6cc80eb5a..4195d4bcb 100644 --- a/rackspace/php-opencloud/docs/userguide/Identity/Service.md +++ b/rackspace/php-opencloud/docs/userguide/Identity/Service.md @@ -1,24 +1,5 @@ # Identity service -## Intro +Our docs have moved! Please visit the below link: -The Identity service is regionless, so you do not need to specify a region when instantiating the service object. Although this was primarily based on Rackspace's implementation of Cloud Identity, it should also work for OpenStack Keystone. - -## A note on object creation - -Normally, when services are created the client handles authenticates automatically. But because Keystone/Identity is fundamental to the authentication process itself, it proves difficult to do this procedure as its normally done. For this reason, you have two options when creating the service object: - -1: Use the client's factory method - -```php -$identity = $client->identityService(); -``` - -2: Authenticate manually - -```php -use OpenCloud\Identity\Service as IdentityService; - -$identity = IdentityService::factory($client); -$identity->getClient()->authenticate(); -``` \ No newline at end of file +http://docs.php-opencloud.com/en/latest/services/identity/index.html diff --git a/rackspace/php-opencloud/docs/userguide/Identity/Tenants.md b/rackspace/php-opencloud/docs/userguide/Identity/Tenants.md index b30cb661d..2129f9604 100644 --- a/rackspace/php-opencloud/docs/userguide/Identity/Tenants.md +++ b/rackspace/php-opencloud/docs/userguide/Identity/Tenants.md @@ -1,21 +1,5 @@ # Tenants -## Intro +Our docs have moved! Please visit the below link: -A tenant is a container used to group or isolate resources and/or identity objects. Depending on the service operator, a tenant may map to a customer, account, organization, or project. - -## Setup - -Tenant objects are instantiated from the Identity service. For more details, see the [Service](Service.md) docs. - -## List tenants - -```php -$tenants = $service->getTenants(); - -foreach ($tenants as $tenant) { - // ... -} -``` - -For more information about how to use iterators, see the [documentation](../Iterators.md). \ No newline at end of file +http://docs.php-opencloud.com/en/latest/services/identity/tenants.html diff --git a/rackspace/php-opencloud/docs/userguide/Identity/Tokens.md b/rackspace/php-opencloud/docs/userguide/Identity/Tokens.md index 04a3ec327..f59835ef4 100644 --- a/rackspace/php-opencloud/docs/userguide/Identity/Tokens.md +++ b/rackspace/php-opencloud/docs/userguide/Identity/Tokens.md @@ -1,84 +1,5 @@ # Tokens -## Intro +Our docs have moved! Please visit the below link: -A token is an opaque string that represents an authorization to access cloud resources. Tokens may be revoked at any time and are valid for a finite duration. - -## Setup - -Token objects are instantiated from the Identity service. For more details, see the [Service](Service.md) docs. - -## Useful object properties/methods - -Property|Description|Getter|Setter ----|---|---|--- -id|The unique ID of the token|`getId()`|`setId()` -expires|Timestamp of when the token will expire|`getExpires()` or `hasExpired()`|`setExpires()` - -## Create token (authenticate) - -In order to generate a token, you must pass in the JSON template that is sent to the API. This is because Rackspace's operation expects a slightly different entity body than OpenStack Keystone. - -Request body for Rackspace's generate token operation: - -```json -{ - "auth": { - "RAX-KSKEY:apiKeyCredentials": { - "username": "foo", - "apiKey": "aaaaa-bbbbb-ccccc-12345678" - }, - "tenantId": "1100111" - } -} -``` - -Request body for Keystone's generate token operation: - -```json -{ - "auth": { - "passwordCredentials":{ - "username":"demoauthor", - "password":"theUsersPassword" - }, - "tenantId": "12345678" - } -} -``` - -The only real differences you'll notice is the name of the object key (`RAX-KSKEY:apiKeyCredentials`/`passwordCredentials`) and the secret (`apiKey`/`password`). The `tenantId` property in both templates are optional. You can also add `tenantName` too. - -```php -use OpenCloud\Common\Http\Message\Formatter; - -$template = sprintf( - '{"auth": {"RAX-KSKEY:apiKeyCredentials":{"username": "%s", "apiKey": "%s"}}}', - 'my_username', - 'my_api_key' -); - -$response = $service->generateToken($template); - -$body = Formatter::decode($response); - -// service catalog -$catalog = $body->access->serviceCatalog; - -// token -$token = $body->access->token; - -// user -$user = $body->access->user; -``` - -As you will notice, these variables will be stdClass objects - for fully fledged functionality, let the client authenticate by itself because it ends up stocking the necessary models for you. - -To see the response body structure, consult the [official docs](http://docs.rackspace.com/auth/api/v2.0/auth-client-devguide/content/POST_authenticate_v2.0_tokens_Token_Calls.html). - -## Revoke token (destroy session) - -```php -$tokenId = '1234567'; -$service->revokeToken($tokenId); -``` \ No newline at end of file +http://docs.php-opencloud.com/en/latest/services/identity/tokens.html diff --git a/rackspace/php-opencloud/docs/userguide/Identity/Users.md b/rackspace/php-opencloud/docs/userguide/Identity/Users.md index 286aa6007..a69fbb7dc 100644 --- a/rackspace/php-opencloud/docs/userguide/Identity/Users.md +++ b/rackspace/php-opencloud/docs/userguide/Identity/Users.md @@ -1,128 +1,5 @@ # Users -## Intro +Our docs have moved! Please visit the below link: -A user is a digital representation of a person, system, or service who consumes cloud services. Users have credentials and may be assigned tokens; based on these credentials and tokens, the authentication service validates that incoming requests are being made by the user who claims to be making the request, and that the user has the right to access the requested resources. Users may be directly assigned to a particular tenant and behave as if they are contained within that tenant. - -## Setup - -User objects are instantiated from the Identity service. For more details, see the [Service](Service.md) docs. - -## Useful object properties/methods - -Property|Description|Getter|Setter ----|---|---|--- -id|The unique ID for this user|`getId()`|`setId()` -username|Username for this user|`getUsername()`|`setUsername()` -email|User's email address|`getEmail()`|`setEmail()` -enabled|Whether or not this user can consume API functionality|`getEnabled()` or `isEnabled()`|`setEnabled()` -password|Either a user-defined string, or an automatically generated one, that provides security when authenticating.|`getPassword()` only valid on creation|`setPassword()` to set local property only. To set password on API (retention), use `updatePassword()`. -defaultRegion|Default region associates a user with a specific regional datacenter. If a default region has been assigned for this user and that user has **NOT** explicitly specified a region when creating a service object, the user will obtain the service from the default region.|`getDefaultRegion()`|`setDefaultRegion()` -domainId|Domain ID associates a user with a specific domain which was assigned when the user was created or updated. A domain establishes an administrative boundary for a customer and a container for a customer's tenants (accounts) and users. Generally, a domainId is the same as the primary tenant id of your cloud account.|`getDomainId()`|`setDomainId()` - -## List users - -```php -$users = $service->getUsers(); - -foreach ($users as $user) { - // ... -} -``` - -For more information about how to use iterators, see the [documentation](../Iterators.md). - -## Get user - -There are various ways to get a specific user: by name, ID and email address. - -```php -use OpenCloud\Identity\Constants\User as UserConst; - -// Get user by name -$user1 = $service->getUser('jamie'); - -// Get user by ID -$user2 = $service->getUser(123456, UserConst::MODE_ID); - -// Get user by email -$user3 = $service->getUser('jamie.hannaford@rackspace.com', UserConst::MODE_EMAIL); -``` - -## Create user - -There are a few things to remember when creating a user: - -* This operation is available only to users who hold the `identity:user-admin` role. This admin can create a user who holds the `identity:default` user role. - -* The created user **will** have access to APIs but **will not** have access to the Cloud Control Panel. - -* Within an account, a maximum of 100 account users can be added. - -* If you attempt to add a user who already exists, an HTTP error 409 results. - -The `username` and `email` properties are required for creating a user. Providing a `password` is optional; if omitted, one will be automatically generated and provided in the response. - -```php -use Guzzle\Http\Exception\ClientErrorResponseException; - -try { - // execute operation - $user = $service->createUser(array( - 'username' => 'newUser', - 'email' => 'foo@bar.com' - )); -} catch (ClientErrorResponseException $e) { - // catch 4xx HTTP errors - echo $e->getResponse()->toString(); -} - -// show generated password -echo $user->getPassword(); -``` - -## Update user - -When updating a user, specify which attribute/property you want to update: - -```php -$user->update(array( - 'email' => 'new_email@bar.com' -)); -``` - -### Updating a user password - -Updating a user password requires calling a distinct method: -```php -$user->updatePassword('password123'); -``` - -## Delete user - -```php -$user->delete(); -``` - -## List credentials - -This operation allows you to see your non-password credential types for all authentication methods available. - -```php -$creds = $user->getOtherCredentials(); -``` - -## Get user API key - -```php -echo $user->getApiKey(); -``` - -## Reset user API key - -When resetting an API key, a new one will be automatically generated for you: - -```php -$user->resetApiKey(); -echo $user->getApiKey(); -``` \ No newline at end of file +http://docs.php-opencloud.com/en/latest/services/identity/users.html diff --git a/rackspace/php-opencloud/docs/userguide/Iterators.md b/rackspace/php-opencloud/docs/userguide/Iterators.md index 553507937..4c8c794ce 100644 --- a/rackspace/php-opencloud/docs/userguide/Iterators.md +++ b/rackspace/php-opencloud/docs/userguide/Iterators.md @@ -1,118 +1,5 @@ # Iterators -## Intro +Our docs have moved! Please visit the below link: -Iterators allow you to traverse over collections of your resources in an efficient and easy way. Currently there are two Iterators provided by the SDK: - -- **ResourceIterator**. The standard iterator class that implements SPL's standard [Iterator](http://php.net/manual/en/class.iterator.php), [ArrayAccess](http://www.php.net/manual/en/class.arrayaccess.php) and [Countable](http://php.net/manual/en/class.countable.php) interfaces. In short, this allows you to traverse this object (using `foreach`), count its internal elements like an array (using `count` or `sizeof`), and access its internal elements like an array (using `$iterator[1]`). - - -- **PaginatedIterator**. This is a child of ResourceIterator, and as such inherits all of its functionality. The difference however is that when it reaches the end of the current collection, it attempts to construct a URL to access the API based on predictive paginated collection templates. - -## Common behaviour - -```php -$iterator = $computeService->flavorList(); -``` - -There are two ways to traverse an iterator. The first is the longer, more traditional way: - -```php -while ($iterator->valid()) { - $flavor = $iterator->current(); - - // do stuff.. - echo $flavor->id; - - $iterator->next(); -} -``` - -There is also a shorter and more intuitive version: - -```php -foreach ($iterator as $flavor) { - // do stuff... - echo $flavor->id; -} -``` - -Because the iterator implements PHP's native `Iterator` interface, it can inherit all the native functionality of traversible data structures with `foreach`. - -## Very important note - -Until now, users have been expected to do this: - -```php -while ($flavor = $iterator->next()) { - // ... -} -``` - -which is **incorrect**. The single responsibility of `next` is to move the internal pointer forward. It is the job of `current` to retrieve the current element. - -For your convenience, these two Iterator classes are fully backward compatible: they exhibit all the functionality you'd expect from a correctly implemented iterator, but they also allow previous behaviour. - -## Using paginated collections - -For large collections, such as retrieving DataObjects from CloudFiles/Swift, you need to use pagination. Each resource will have a different limit per page; so once that page is traversed, there needs to be another API call to retrieve to *next* page's resources. - -There are two key concepts: - -- **limit** is the amount of resources returned per page -- **marker** is the way you define a starting point. It is some form of identifier that allows the collection to begin from a specific resource - -### Resource classes - -When the iterator returns a current element in the internal list, it populates the relevant resource class with all the data returned to the API. In most cases, a `stdClass` object will become an instance of `OpenCloud\Common\PersistentObject`. - -In order for this instantiation to happen, the `resourceClass` option must correspond to some method in the parent class that creates the resource. For example, if we specify 'ScalingPolicy' as the `resourceClass`, the parent object (in this case `OpenCloud\Autoscale\Group`, needs to have some method will allows the iterator to instantiate the child resource class. These are all valid: - -1. `Group::scalingGroup($data);` - -2. `Group::getScalingGroup($data);` - -3. `Group::resource('ScalingGroup', $data);` - -where `$data` is the standard object. This list runs in order of precedence. - -## Setting up a PaginatedIterator - -```php -use OpenCloud\Common\Collection\PaginatedIterator; - -$service = $client->computeService(); - -$flavors = PaginatedIterator::factory($service, array( - 'resourceClass' => 'Flavor', - 'baseUrl' => $service->getUrl('flavors') - 'limit.total' => 350, - 'limit.page' => 100, - 'key.collection' => 'flavors' -)); - -foreach ($flavors as $flavor) { - echo $flavor->getId(); -} -``` - -As you can see, there are a lot of configuration parameters to pass in - and getting it right can be quite fiddly, involving a lot of API research. For this reason, using the convenience methods like `flavorList` is recommended because it hides the complexity. - -### PaginatedIterator options - -There are certain configuration options that the paginated iterator needs to work. These are: - -Name|Description|Type|Required|Default| ----|---|---|---|---| -resourceClass|The resource class that is instantiated when the current element is retrieved. This is relative to the parent/service which called the iterator.|string|Yes|- -baseUrl|The base URL that is used for making new calls to the API for new pages|`Guzzle\Http\Url`|Yes|- -limit.total|The total amount of resources you want to traverse in your collection. The iterator will stop as this limit is reached, regardless if there are more items in the list|int|No|10000 -limit.page|The amount of resources each page contains|int|No|100 -key.links|Often, API responses will contain "links" that allow easy access to the next page of a resource collection. This option specifies what that JSON element is called (its key). For example, for Rackspace Compute images it is `images_links`.|string|No|links -key.collection|The top-level key for the array of resources. For example, servers are returned with this data structure: `{"servers": [...]}`. The **key.collection** value in this case would be `servers`.|string|No|`null` -key.collectionElement|Rarely used. But it indicates the key name for each nested resource element. KeyPairs, for example, are listed like this: `{"keypairs": [ {"keypair": {...}} ] }`. So in this case the collectionElement key would be `keypair`.|string|No|`null` -key.marker|The value used as the marker. It needs to represent a valid property in the JSON resource objects. Often it is `id` or `name`.|string|No|name -request.method|The HTTP method used when making API calls for new pages|string|No|GET -request.headers|The HTTP headers to send when making API calls for new pages|array|No|`array()` -request.body|The HTTP entity body to send when making API calls for new pages|`Guzzle\Http\EntityBody`|No|`null` -request.curlOptions|Additional cURL options to use when making API calls for new pages|array|No|`array()` +http://docs.php-opencloud.com/en/latest/iterators.html diff --git a/rackspace/php-opencloud/docs/userguide/LoadBalancer/README.md b/rackspace/php-opencloud/docs/userguide/LoadBalancer/README.md index 2865be29e..3531e3dc8 100644 --- a/rackspace/php-opencloud/docs/userguide/LoadBalancer/README.md +++ b/rackspace/php-opencloud/docs/userguide/LoadBalancer/README.md @@ -1,80 +1,5 @@ # Load Balancers -A **load balancer** is a device that distributes incoming network traffic amongst -multiple back-end systems. These back-end systems are called the **nodes** of -the load balancer. +Our docs have moved! Please visit the below link: -## Getting started - -### 1. Instantiate a Rackspace client. - -```php - -use OpenCloud\Rackspace; - -$client = new Rackspace(Rackspace::US_IDENTITY_ENDPOINT, array( - 'username' => ', - 'apiKey' => '' -)); -``` - -### 2. Retrieve the server instances you want to add as nodes of the load balancer. - -```php -$computeService = $client->computeService('cloudServersOpenStack', 'DFW'); - -$serverOne = $computeService->server('e836fc4e-056d-4447-a80e-fefcaa640216'); -$serverTwo = $computeService->server('5399cd36-a23f-41a6-bdf7-20902aec0e74'); -``` - -The example above uses two server instances that have already been created. It -retrieves the server instances using their IDs. See also: [creating server instances](). - -### 3. Obtain a Load Balancer service object from the client. - -This object will be used to first define the load balancer nodes and later create the load balancer itself. - -```php -$loadBalancerService = $client->loadBalancerService('cloudLoadBalancers', 'DFW'); -``` - -### 4. Define a load balancer node for each server. - -```php -$loadBalancer = $loadBalancerService->loadBalancer(); - -$serverOneNode = $loadBalancer->node(); -$serverOneNode->address = $serverOne->addresses->private[0]->addr; -$serverOneNode->port = 8080; -$serverOneNode->condition = 'ENABLED'; - -$serverTwoNode = $loadBalancer->node(); -$serverTwoNode->address = $serverTwo->addresses->private[0]->addr; -$serverTwoNode->port = 8080; -$serverTwoNode->condition = 'ENABLED'; -``` - -In the example above, each node runs a service that listens on port 8080. Further, -each node will start out as `ENABLED`, which means it will be ready to receive -network traffic from the load balancer as soon as it is created. - -### 5. Create the load balancer with the two nodes. - -```php -$loadBalancer->addVirtualIp('PUBLIC'); -$loadBalancer->create(array( - 'name' => 'My smart load balancer', - 'port' => 80, - 'protocol' => 'HTTP', - 'nodes' => array($serverOneNode, $serverTwoNode) -)); -``` - -In the example above, the load balancer will have a virtual IP address accessible -from the public Internet. Also notice that the port the load balancer listens -on (80) does not need to match the ports of its nodes (8080). - -## Next steps - -Once you have created a load balancer, there is a lot you can do with it. See -the [complete user guide for load balancers](USERGUIDE.md). \ No newline at end of file +http://docs.php-opencloud.com/en/latest/services/load-balancer/index.html diff --git a/rackspace/php-opencloud/docs/userguide/LoadBalancer/USERGUIDE.md b/rackspace/php-opencloud/docs/userguide/LoadBalancer/USERGUIDE.md index caee48be2..d29925da0 100644 --- a/rackspace/php-opencloud/docs/userguide/LoadBalancer/USERGUIDE.md +++ b/rackspace/php-opencloud/docs/userguide/LoadBalancer/USERGUIDE.md @@ -1,307 +1,5 @@ # The Complete User Guide to Load Balancers -## Prerequisites +Our docs have moved! Please visit the below link: -### Client -To use the load balancers service, you must first instantiate a `Rackspace` -client object. - -```php -use OpenCloud\Rackspace; - -$client = new Rackspace(Rackspace::US_IDENTITY_ENDPOINT, array( - 'username' => ', - 'apiKey' => '' -)); -``` - -### Load Balancer Service -All operations on load balancers are done via a load balancer service object. - -```php -$loadBalancerService = $client->loadBalancerService('cloudLoadBalancers', 'DFW'); -``` - -### Cloud Servers -Many of the examples in this document use two cloud servers as nodes for -the load balancer. The variables `$serverOne` and `$serverTwo` refer to these -two cloud servers. - -## Load Balancers - -A **load balancer** is a device that distributes incoming network traffic amongst -multiple back-end systems. These back-end systems are called the **nodes** of -the load balancer. - -### Create Load Balancer - -```php -$loadBalancer = $loadBalancerService->loadBalancer(); - -$serverOneNode = $loadBalancer->node(); -$serverOneNode->address = $serverOne->addresses->private[0]->addr; -$serverOneNode->port = 8080; -$serverOneNode->condition = 'ENABLED'; - -$serverTwoNode = $loadBalancer->node(); -$serverTwoNode->address = $serverTwo->addresses->private[0]->addr; -$serverTwoNode->port = 8080; -$serverTwoNode->condition = 'ENABLED'; - -$loadBalancer->addVirtualIp('PUBLIC'); -$loadBalancer->create(array( - 'name' => 'My smart load balancer', - 'port' => 80, - 'protocol' => 'HTTP', - 'nodes' => array($serverOneNode, $serverTwoNode) -)); -``` - -### List Load Balancer Details - -You can retrieve a single load balancer's details by using its ID. - -```php -$loadBalancer = $loadBalancerService->loadBalancer('254889'); - -/** @var $loadBalancer OpenCloud\LoadBalancer\Resource\LoadBalancer **/ -``` - -### List Load Balancers - -You can retrieve a list of all your load balancers. An instance of `OpenCloud\Common\Collection\PaginatedIterator` -is returned. - -```php -$loadBalancers = $loadBalancerService->loadBalancerList(); -foreach ($loadBalancers as $loadBalancer) { - /** @var $loadBalancer OpenCloud\LoadBalancer\Resource\LoadBalancer **/ -} -``` - -### Update Load Balancer Attributes - -You can update one or more of the following load balancer attributes: - -* `name`: The name of the load balancer -* `algorithm`: The algorithm used by the load balancer to distribute traffic amongst its nodes. See also: [Load balancing algorithms](#algorithms). -* `protocol`: The network protocol used by traffic coming in to the load balancer. See also: [Protocols](#protocols). -* `port`: The network port on which the load balancer listens for incoming traffic. -* `halfClosed`: Enable or Disable Half-Closed support for the load balancer. -* `timeout`: The timeout value for the load balancer to communicate with its nodes. -* `httpsRedirect`: Enable or disable HTTP to HTTPS redirection for the load balancer. When enabled, any HTTP request will return status code 301 (Moved Permanently), and the requestor will be redirected to the requested URL via the HTTPS protocol on port 443. For example, http://example.com/page.html would be redirected to https:// example.com/page.html. Only available for HTTPS protocol (`port` = 443), or HTTP Protocol with a properly configured SSL Termination (`secureTrafficOnly=true, securePort=443). See also: [SSL Termination](#ssl-termination). - -#### Updating a single attribute of a load balancer -```php -$loadBalancer->update(array( - 'name' => 'New name' -)); -``` - -#### Updating multiple attributes of a load balancer -```php -$loadBalancer->update(array( - 'name' => 'New name', - 'algorithm' => 'ROUND_ROBIN' -)); -``` - -### Remove Load Balancer - -When you no longer have a need for the load balancer, you can remove it. - -```php -$loadBalancer->delete(); -``` - -## Nodes - -A **node** is a backend device that provides a service on specified IP and port. An example of a load balancer node might be a web server serving HTTP traffic on port 8080. - -A load balancer typically has multiple nodes attached to it so it can distribute incoming network traffic amongst them. - -### List Nodes - -You can list the nodes attached to a load balancer. An instance of `OpenCloud\Common\Collection\PaginatedIterator` -is returned. - -```php -$nodes = $loadBalancer->listNodes(); -foreach ($nodes as $node) { - /** @var $node OpenCloud\LoadBalancer\Resource\Node **/ -} -``` - -### Add Nodes - -You can attach additional nodes to a load balancer. Assume `$loadBalancer` already has two nodes attached to it - `$serverOne` and `$serverTwo` - and you want to attach a third node to it, say `$serverThree`, which provides a service on port 8080. - -**Important:** Remember to call `$loadBalancer->addNodes()` after all the calls to `$loadBalancer->addNode()` as shown below. - -```php -$address = $serverThree->addresses->private[0]->addr; -$loadBalancer->addNode($address, 8080); -$loadBalancer->addNodes(); -``` - -The `addNode` method accepts three more optional parameters, in addition to the two shown above: - -| Position | Description | Data type | Required? | Default value | -| ----------- | --------------- | --------------| -------------- | ----------------- | -| 1 | IP address of node | String | Yes | - | -| 2 | Port used by node's service | Integer | Yes | - | -| 3 | Starting condition of node:
    • `ENABLED` – Node is ready to receive traffic from the load balancer.
    • `DISABLED` – Node should not receive traffic from the load balancer.
    • `DRAINING` – Node should process any traffic it is already receiving but should not receive any further traffic from the load balancer.
    | String | No | `ENABLED` | -| 4 | Type of node to add:
    • `PRIMARY` – Nodes defined as PRIMARY are in the normal rotation to receive traffic from the load balancer.
    • `SECONDARY` – Nodes defined as SECONDARY are only in the rotation to receive traffic from the load balancer when all the primary nodes fail.
    | String | No | `PRIMARY` | -| 5 | Weight, between 1 and 100, given to node when distributing traffic using either the `WEIGHTED_ROUND_ROBIN` or the `WEIGHTED_LEAST_CONNECTIONS` load balancing algorithm. | Integer | No | 1 | - -### Modify Nodes -You can modify one or more of the following node attributes: - -* `condition`: The condition of the load balancer: - * `ENABLED` – Node is ready to receive traffic from the load balancer. - * `DISABLED` – Node should not receive traffic from the load balancer. - * `DRAINING` – Node should process any traffic it is already receiving but should not receive any further traffic from the load balancer. -* `type`: The type of the node: - * `PRIMARY` – Nodes defined as PRIMARY are in the normal rotation to receive traffic from the load balancer. - * `SECONDARY` – Nodes defined as SECONDARY are only in the rotation to receive traffic from the load balancer when all the primary nodes fail. -* `weight`: The weight, between 1 and 100, given to node when distributing traffic using either the `WEIGHTED_ROUND_ROBIN` or the `WEIGHTED_LEAST_CONNECTIONS` load balancing algorithm. - -#### Modifying a single attribute of a node -```php -$node->update(array( - 'condition' => 'DISABLED' -)); -``` - -#### Modifying multiple attributes of a node -```php -$node->update(array( - 'condition' => 'DISABLED', - 'type' => 'SECONDARY' -)); -``` - -### Remove Nodes - -There are two ways to remove a node. - -#### Given an `OpenCloud\LoadBalancer\Resource\Node` instance -```php -$node->delete(); -``` - -#### Given an `OpenCloud\LoadBalancer\Resource\LoadBalancer` instance and a node ID -```php -$loadBalancer->removeNode(490639); -``` - -The `removeNode` method, as shown above, accepts the following arguments: - -|Position| Description | Data type | Required? | Default value | -|----------- | --------------- | -------------- |-------------- | ----------------- | -| 1 | ID of node | Integer | Yes | - | - -### View Node Service Events -You can view events associated with the activity between a node and a load balancer. An instance of `OpenCloud\Common\Collection\PaginatedIterator` is returned. - -```php -$nodeEvents = $loadBalancer->nodeEventList(); -foreach ($nodeEvents as $nodeEvent) { - /** @var $nodeEvent OpenCloud\LoadBalancer\Resource\NodeEvent **/ -} -``` - -## Virtual IPs - -A **virtual IP (VIP)** makes a load balancer accessible by clients. The load balancing service supports either a public VIP address (`PUBLIC`), routable on the public Internet, or a ServiceNet VIP address (`SERVICENET`), routable only within the region in which the load balancer resides. - -### List Virtual IPs - -You can list the VIPs associated with a load balancer. An instance of `OpenCloud\Common\Collection\PaginatedIterator` is returned. - -```php -$vips = $loadBalancer->virtualIpList(); -foreach ($vips as $vip) { - /** @var $vip of OpenCloud\LoadBalancer\Resource\VirtualIp **/ -} -``` - -### Add Virtual IPv6 - -You can add additional IPv6 VIPs to a load balancer. - -```php -$loadBalancer->addVirtualIp('PUBLIC', 6); -``` - -The `addVirtualIp` method, as shown above, accepts the following arguments: - -| Position | Description | Data type | Required? | Default value | -| ----------- | --------------- | -------------- |-------------- | ----------------- | -| 1 | Type of VIP:
    • `PUBLIC` – An IP address that is routable on the public Internet.
    • `SERVICENET` – An IP address that is routable only on ServiceNet.
    | String | No | `PUBLIC` | -| 2 | IP version: Must be `6` | Integer | Yes | - | - - -### Remove Virtual IPs - -You can remove a VIP from a load balancer. - -```php -$vip->remove(); -``` - -Please note that a load balancer must have at least one VIP associated with it. If you try to remove a load balancer's last VIP, a `ClientErrorResponseException` will be thrown. - -## Algorithms - -Load balancers use an **algorithm** to determine how incoming traffic is distributed amongst the back-end nodes. - -### List Load Balancing Algorithms - -You can list all supported load balancing algorithms using a load balancer service object. An instance of `OpenCloud\Common\Collection\PaginatedIterator` is returned. - -```php -$algorithms = $loadBalancerService->algorithmList(); -foreach ($algorithms as $algorithm) { - /** @var $algorithm OpenCloud\LoadBalancer\Resource\Algorithm **/ -} -``` - -## Protocols - -When a load balancer is created a network protocol must be specified. This network protocol should be based on the network protocol of the back-end service being load balanced. - -### List Load Balancing Protocols - -You can list all supported network protocols using a load balancer service object. An instance of `OpenCloud\Common\Collection\PaginatedIterator` is returned. - -```php -$protocols = $loadBalancerService->protocolList(); -foreach ($protocols as $protocol) { - /** @var $protocol OpenCloud\LoadBalancer\Resource\Protocol **/ -} -``` - -## Sessions - -### Manage Session Persistence - -## Connections - -## Error Page - -## Allowed Domains - -## Access Lists - -## Content Caching - -## SSL Termination - -## Metadata - -## Monitors - -## Statistics - -## Usage Reports \ No newline at end of file +http://docs.php-opencloud.com/en/latest/services/load-balancer/index.html diff --git a/rackspace/php-opencloud/docs/userguide/ObjectStore/Access.md b/rackspace/php-opencloud/docs/userguide/ObjectStore/Access.md old mode 100755 new mode 100644 index 0b15a2d19..53466362e --- a/rackspace/php-opencloud/docs/userguide/ObjectStore/Access.md +++ b/rackspace/php-opencloud/docs/userguide/ObjectStore/Access.md @@ -1,66 +1,5 @@ ## Setup -```php -use OpenCloud\Rackspace; +Our docs have moved! Please visit the below link: -$client = new Rackspace(RACKSPACE_US, array( - -)); - -$service = $client->objectStoreService('cloudFiles', 'IAD'); # Second argument is the region you want -``` - -## Temporary URLs - -Temporary URLs allow you to create time-limited Internet addresses that allow you to grant access to your Cloud Files -account. Using Temporary URL, you may allow others to retrieve or place objects in your containers - regardless of -whether they're CDN-enabled. - -### Set "temporary URL" metadata key - -You must set this "secret" value on your account, where it can be used in a global state: - -```php -$account = $service->getAccount(); -$account->setTempUrlSecret('my_secret'); - -echo $account->getTempUrlSecret(); -``` - -The string argument of `setTempUrlSecret()` is optional - if left out, the SDK will generate a random hashed secret -for you. - -### Create a temporary URL - -Once you've set an account secret, you can create a temporary URL for your object. To allow GET access to your object -for 1 minute: - -```php -$object->getTemporaryUrl(60, 'GET'); -``` - -To allow PUT access for 1 hour: - -```php -$object->getTemporaryUrl(360, 'PUT'); -``` - -## Hosting websites on CloudFiles - -To host a static (i.e. HTML) website on CloudFiles, you must follow these steps: - -1. CDN-enable a container -2. Upload all HTML content. You can use nested directory structures. -3. Tell CloudFiles what to use for your default index page like this: - -```php -$container->setStaticIndexPage('index.html'); -``` - -4. (Optional) Tell CloudFiles which error page to use by default: - -```php -$container->setStaticErrorPage('error.html'); -``` - -Bear in mind that steps 3 & 4 do not upload content, but rather specify a reference to an existing page/CloudFiles object. \ No newline at end of file +http://docs.php-opencloud.com/en/latest/services/object-store/access.html diff --git a/rackspace/php-opencloud/docs/userguide/ObjectStore/Account.md b/rackspace/php-opencloud/docs/userguide/ObjectStore/Account.md old mode 100755 new mode 100644 index edd26c2c1..286bb9ca1 --- a/rackspace/php-opencloud/docs/userguide/ObjectStore/Account.md +++ b/rackspace/php-opencloud/docs/userguide/ObjectStore/Account.md @@ -1,28 +1,5 @@ ## Setup -```php -use OpenCloud\Rackspace; +Our docs have moved! Please visit the below link: -$client = new Rackspace(RACKSPACE_US, array( - -)); - -$service = $client->objectStoreService('cloudFiles'); -``` - -## View Account Details - -To see how many containers you have in your account (X-Account-Container-Count), how many objects are in your account -(X-Account-Object-Count), and how many total bytes your account uses (X-Account-Bytes-Used): - -```php -$account = $service->getAccount(); - -// Either return the full Metadata object -$details = $account->getDetails(); - -// or individual values -$account->getContainerCount(); -$account->getObjectCount(); -$account->getBytesUsed(); -``` \ No newline at end of file +http://docs.php-opencloud.com/en/latest/services/object-store/account.html diff --git a/rackspace/php-opencloud/docs/userguide/ObjectStore/CDN/Container.md b/rackspace/php-opencloud/docs/userguide/ObjectStore/CDN/Container.md old mode 100755 new mode 100644 index 3d06216fc..c854b31e5 --- a/rackspace/php-opencloud/docs/userguide/ObjectStore/CDN/Container.md +++ b/rackspace/php-opencloud/docs/userguide/ObjectStore/CDN/Container.md @@ -1,69 +1,5 @@ ## Setup -```php -use OpenCloud\Rackspace; +Our docs have moved! Please visit the below link: -$client = new Rackspace(RACKSPACE_US, array( - -)); - -$service = $client->objectStoreService('cloudFiles'); -``` - -To access the CDN functionality of a particular container: - -```php -$container = $service->getContainer('foo_bar'); - -$cdn = $container->getCdn(); -``` - -## List CDN-enabled container - -To list CDN-only containers, follow the same operation for Storage which lists all containers. The only difference is -which service object you execute the method on. - -## CDN-enable and -disable a container - -Before a container can be CDN-enabled, it must exist in the storage system. When a container is CDN-enabled, any objects -stored in it are publicly accessible over the Content Delivery Network by combining the container's CDN URL with the -object name. - -Any CDN-accessed objects are cached in the CDN for the specified amount of time called the TTL. The default TTL value is -259200 seconds, or 72 hours. Each time the object is accessed after the TTL expires, the CDN refetches and caches the -object for the TTL period. - -```php -$container->enableCdn(); -$container->disableCdn(); -``` - -## Serving containers through SSL - -```php -$cdn->getCdnSslUri(); -``` - -## Streaming CDN-enabled containers - -```php -$cdn->getCdnStreamingUri(); -``` - -## iOS streaming - -The Cloud Files CDN allows you to stream video to iOS devices without needing to convert your video. Once you -CDN-enable your container, you have the tools necessary for streaming media to multiple devices. - -```php -$cdn->getIosStreamingUri(); -``` - -## CDN logging - -To enable and disable logging for your CDN: - -```php -$cdn->enableCdnLogging(); -$cdn->disableCdnLogging(); -``` \ No newline at end of file +http://docs.php-opencloud.com/en/latest/services/object-store/cdn.html diff --git a/rackspace/php-opencloud/docs/userguide/ObjectStore/CDN/Object.md b/rackspace/php-opencloud/docs/userguide/ObjectStore/CDN/Object.md old mode 100755 new mode 100644 index 79e3d4cdc..c854b31e5 --- a/rackspace/php-opencloud/docs/userguide/ObjectStore/CDN/Object.md +++ b/rackspace/php-opencloud/docs/userguide/ObjectStore/CDN/Object.md @@ -1,20 +1,5 @@ ## Setup -You will need to instantiate the container object and access its CDN functionality as -[documented here](https://github.com/rackspace/php-opencloud/blob/master/docs/userguide/ObjectStore/CDN/Container.md). +Our docs have moved! Please visit the below link: -## Purge CDN-enabled objects - -To remove a CDN object from public access: - -```php -$object->purge(); -``` - -You can also provide an optional e-mail address (or comma-delimeted list of e-mails), which the API will send a -confirmation message to once the object has been completely purged: - -```php -$object->purge('jamie.hannaford@rackspace.com'); -$object->purge('hello@example.com,hallo@example.com'); -``` +http://docs.php-opencloud.com/en/latest/services/object-store/cdn.html diff --git a/rackspace/php-opencloud/docs/userguide/ObjectStore/Storage/Container.md b/rackspace/php-opencloud/docs/userguide/ObjectStore/Storage/Container.md old mode 100755 new mode 100644 index d30cd0796..ab705fa15 --- a/rackspace/php-opencloud/docs/userguide/ObjectStore/Storage/Container.md +++ b/rackspace/php-opencloud/docs/userguide/ObjectStore/Storage/Container.md @@ -1,181 +1,5 @@ ## Setup -```php -use OpenCloud\Rackspace; +Our docs have moved! Please visit the below link: -// Create a client object to communicate with various Rackspace Cloud services. -$client = new Rackspace(RACKSPACE_US, array( - 'username' => 'Replace this with your Rackspace Cloud user name', - 'apiKey' => 'Replace this with your Rackspace Cloud API key' -)); - -// Create a service object to use the object store service. The sample code -// creates the object store in the 'DFW' region. -$service = $client->objectStoreService('cloudFiles', 'DFW'); -``` - -## Create container - -To create a new container, you just need to define its name: - -```php -$container = $service->createContainer('my_amazing_container'); -``` - -If the response returned is `FALSE`, there was an API error - most likely due to the fact you have a naming collision. - -Container names must be valid strings between 0 and 256 characters. Forward slashes are not currently permitted. - - -## List containers - -### Return a list of containers - -```php -$containerList = $service->listContainers(); - -while ($container = $containerList->next()) { - // Do stuff; some examples below - printf("Container name: %s\n", $container->name); - printf("Number of objects within container: %d\n", $container->getObjectCount()); -} -``` - -Container names are sorted based on a binary comparison, a single built-in collating sequence that compares string -data using SQLite's memcmp() function, regardless of text encoding. - -The list is limited to 10,000 containers at a time. See 1.3 for ways to limit and navigate this list. - -### Return a formatted list of containers - -Currently, the SDK only supports JSON-formatted responses. - -### Controlling a large list of containers - -You may limit and control this list of results by using the `marker` and `end_marker` parameters. The former parameter -(`marker`) tells the API where to begin the list, and the latter (`end_marker`) tells it where to end the list. You may -use either of them independently or together. You may also use the `limit` parameter to fix the number of containers -returned. - -To list a set of containers between two fixed points: - -```php -$someContainers = $service->listContainers(array( - 'marker' => 'container_55', - 'end_marker' => 'container_2001' -)); -``` - -Or to return a limited set: - -```php -$someContainers = $service->listContainers(array('limit' => 560)); -``` - -## Get container - -To retrieve a certain container, either to access its object or metadata: - -```php -$container = $service->getContainer('container_name'); - -echo $container->getObjectCount(); -echo $container->getBytesUsed(); -``` - -## Delete container - -Deleting a container is easy: -```php -$container->delete(); -``` - -Please bear mind that you must delete all objects inside a container before deleting it. This is done for you if you -set the `$deleteObjects` parameter to `TRUE` like so: - -```php -$container->delete(TRUE); -``` - -You can also do it manually: - -```php -$container->deleteAllObjects(); -$container->delete(); -``` - -## Create or update container metadata - -```php -$container->saveMetadata(array( - 'Author' => 'Virginia Woolf', - 'Published' => '1931' -)); -``` - -Please bear in mind that this action will set metadata to this array - overriding existing values and wiping those left -out. To _append_ values to the current metadata: - -```php -$metadata = $container->appendToMetadata(array( - 'Publisher' => 'Hogarth' -)); -``` - -If you only want to set the metadata to the local object, and not immediately retain these values on the API, you can -use a standard setter method - which can contribute to eventual actions like an update: - -```php -$container->setMetadata(array('Foo' => 'Bar')); -``` - -## Container quotas - -The container_quotas middleware implements simple quotas that can be imposed on Cloud Files containers by a user. -Setting container quotas can be useful for limiting the scope of containers that are delegated to non-admin users, -exposed to formpost uploads, or just as a self-imposed sanity check. - -To set quotas for a container: - -```php -use OpenCloud\Common\Constants\Size; - -$container->setCountQuota(1000); -$container->setBytesQuota(2.5 * Size::GB); -``` - -And to retrieve them: - -```php -echo $container->getCountQuota(); -echo $container->getBytesQuota(); -``` - -## Access log delivery - -To view your object access, turn on Access Log Delivery. You can use access logs to analyze the number of people who -access your objects, where they come from, how many requests for each object you receive, and time-based usage patterns -(such as monthly or seasonal usage). - -```php -$container->enableLogging(); -$container->disableLogging(); -``` - -## Syncing containers - -You can synchronize local directories with your CloudFiles/Swift containers very easily. When you do this, the container -will mirror exactly the nested file structure within your local directory: - -```php -$container->uploadDirectory('/home/Jamie/blog'); -``` - -There are four scenarios you should be aware of: - -Local|Remote|Comparison|Action ----|---|---|--- -File exists|File exists|Identical checksum|No action -File exists|File exists|Different checksum|Local file overwrites remote -File exists|File does not exist|-|Local file created in Swift -Files does not exist|File exists|-|Remote file deleted \ No newline at end of file +http://docs.php-opencloud.com/en/latest/services/object-store/containers.html diff --git a/rackspace/php-opencloud/docs/userguide/ObjectStore/Storage/Migrating.md b/rackspace/php-opencloud/docs/userguide/ObjectStore/Storage/Migrating.md index d92528cbf..550268f60 100644 --- a/rackspace/php-opencloud/docs/userguide/ObjectStore/Storage/Migrating.md +++ b/rackspace/php-opencloud/docs/userguide/ObjectStore/Storage/Migrating.md @@ -1,83 +1,5 @@ # Migrating containers (across regions) -## Introduction +Our docs have moved! Please visit the below link: -Currently, there exists no single API operation to copy containers across geographic endpoints. Although the API offers -a `COPY` operation for individual files, this does not work for cross-region copying. The SDK, however, does offer this -functionality. - -You **will** be charged for bandwidth between regions, so it's advisable to use ServiceNet where possible (which is -free). - -## Requirements - -* You must install the full Guzzle package, so that the process can take advantage of Guzzle's batching functionality -(it allows parallel requests to be batched for greater efficiency). You can do this by running: - -```bash -php composer.phar install --dev -``` - -* Depending on the size and number of transfer items, you will need to raise PHP's memory limit: - -```php -ini_set('memory_limit', '512M'); -``` - -* You will need to enact some kind of backoff/retry strategy for rate limits. Guzzle comes with a convenient feature -that just needs to be added as a normal subscriber: - -```php -use Guzzle\Plugin\Backoff\BackoffPlugin; - -$client->addSubscriber(BackoffPlugin::getExponentialBackoff(10, array(500, 503, 408))); -``` - -This tells the client to retry up to `10` times for failed requests have resulted in these HTTP status codes: `500`, -`503` or `408`. - -## Setup - -You can access all this functionality by executing: - -```php -$ordService = $client->objectStoreService('cloudFiles', 'ORD'); -$iadService = $client->objectStoreService('cloudFiles', 'IAD'); - -$oldContainer = $ordService->getContainer('old_container'); -$newContainer = $iadService->getContainer('new_container'); - -$iadService->migrateContainer($oldContainer, $newContainer); -``` - -It's advisable to do this process in a Cloud Server in one of the two regions you're migrating to/from. This allows you -to use `privateURL` as the third argument in the `objectStoreService` methods like this: - -```php -$client->objectStoreService('cloudFiles', 'IAD', 'privateURL'); -``` - -This will ensure that traffic between your server and your new IAD container will be held over the internal Rackspace -network which is free. - -## Options - -You can pass in an array of arguments to the method: - -```php -$options = array( - 'read.batchLimit' => 100, - 'read.pageLimit' => 100, - 'write.batchLimit' => 50 -); - -$iadService->migrateContainer($oldContainer, $newContainer, $options); -``` - -### Options explained - -Name|Description|Default ----|---|--- -`read.pageLimit`|When the process begins, it has to collect all the files that exist in the old container. It does this through a conventional `objectList` method, which calls the `PaginatedIterator`. This iterator has the option to specify the page size for the collection (i.e. how many items are contained per page in responses from the API)|10,000 -`read.batchLimit`|After the data objects are collected, the process needs to send an individual GET request to ascertain more information. In order to make this process faster, these individual GET requests are batched together and sent in parallel. This limit refers to how many of these GET requests are batched together.|1,000 -`write.batchLimit`|Once each file has been retrieved from the API, a PUT request is executed against the new container. Similar to above, these PUT requests are batched - and this number refers to the amount of PUT requests batched together.|100 \ No newline at end of file +http://docs.php-opencloud.com/en/latest/services/object-store/migrating-containers.html diff --git a/rackspace/php-opencloud/docs/userguide/ObjectStore/Storage/Object.md b/rackspace/php-opencloud/docs/userguide/ObjectStore/Storage/Object.md old mode 100755 new mode 100644 index d8788aca9..2ecca81e1 --- a/rackspace/php-opencloud/docs/userguide/ObjectStore/Storage/Object.md +++ b/rackspace/php-opencloud/docs/userguide/ObjectStore/Storage/Object.md @@ -1,263 +1,5 @@ ## Setup -Conceptually, a container contains objects (also known as files). In order to work with -objects, you will need to instantiate a container object first as [documented here](https://github.com/rackspace/php-opencloud/blob/master/docs/userguide/ObjectStore/Storage/Container.md). +Our docs have moved! Please visit the below link: -## Note on object properties - -Please be aware that you cannot directly access the properties of DataObject anymore, you __must__ use appropriate getter/ -setter methods: - -|Property|Method| -|--------|------| -|Parent container|`getContainer`| -|Name|`getName`| -|Body of file|`getContent`| -|Size of file|`getContentLength`| -|Type of file|`getContentType`| -|ETag checksum|`getEtag`| -|Last modified date|`getLastModified`| - -## Create an object - -There are three ways to upload a new file, each of which has different business needs. - -__N.B__: Unlike previous versions, you do not need to manually specify your object's content type. The API will do this -for you. - -### To upload a single/basic file: - -```php -use OpenCloud\ObjectStore\Resource\DataObject; - -$data = fopen('/path/to/sample.mp3', 'r+'); - -// alternatively, you can pass in a string as the file contents `$data` argument (instead of a resource) - -$meta = array( - 'Author' => 'Camera Obscura', - 'Origin' => 'Glasgow' -); - -$metaHeaders = DataObject::stockHeaders($meta); -$customHeaders = array(); -$allHeaders = $metaHeaders + $customHeaders; - -$container->uploadObject('sample.mp3', $data, $allHeaders); -``` - -### To upload multiple small-to-mid sized files: - -```php -$files = array( - array( - 'name' => 'apache.log', - 'path' => '/etc/httpd/logs/error_log' - ), - array( - 'name' => 'mysql.log', - 'body' => fopen('/tmp/mysql.log', 'r+') - ), - array( - 'name' => 'to_do_list.txt', - 'body' => 'PHONE HOME' - ) -); - -$container->uploadObjects($files); -``` - -As you can see, the `name` key is required for every file. You must also specify _either_ a path key (to an existing -file), or a `body`. The `body` can either be a PHP resource or a string representation of the content you want to upload. - -### To upload large files - -For files over 5GB, you will need to use the `OpenCloud\ObjectStore\Upload\TransferBuilder` factory to build your transfer, -upon which you can execute your upload functionality. For your convenience, the Container resource object contains a -simple method to do this heavy lifting for you: - -```php -$transfer = $container->setupObjectTransfer(array( - 'name' => 'video.mov', - 'path' => '/home/jamie/video.mov', - 'metadata' => array( - 'Author' => 'Jamie' - ), - 'concurrency' => 4, - 'partSize' => 1.5 * Size::GB -)); - -$transfer->upload(); -``` - -You can specify how many concurrent cURL connections are used to upload parts of your file. The file is fragmented into -chunks, each of which is uploaded individually as a separate file (the filename of each part will indicate that it's a -segment rather than the full file). After all parts are uploaded, a manifest is uploaded. When the end-user accesses -the 5GB by its true filename, it actually references the manifest file which concatenates each segment into a streaming -download. - -## List objects in a container - -To return a list of objects: - -```php -$files = $container->objectList(); -while ($file = $files->next()) { - // ... do something -} -``` - -By default, 10,000 objects are returned as a maximum. To get around this, you can construct a query which refines your -result set. For a full specification of query parameters relating to collection filtering, see the [official docs](http://docs.openstack.org/api/openstack-object-storage/1.0/content/list-objects.html). - -```php -$container->objectList(array('prefix' => 'logFile_')); -``` - -## Get object - -To retrieve a specific file from Cloud Files: - -```php -$file = $container->getObject('summer_vacation.mp4'); -``` - -### Conditional requests - -You can also perform conditional requests according to [RFC 2616 specification](http://www.ietf.org/rfc/rfc2616.txt) -(§§ 14.24-26). Supported headers are `If-Match`, `If-None-Match`, `If-Modified-Since` and `If-Unmodified-Since`. - -So, to retrieve a file's contents only if it's been recently changed - -```php -$file = $container->getObject('error_log.txt', array( - 'If-Modified-Since' => 'Tue, 15 Nov 1994 08:12:31 GMT' -)); - -if ($file->getContentLength()) { - echo 'Has been changed since the above date'; -} else { - echo 'Has not been changed'; -} -``` - -Retrieve a file only if it has NOT been modified (and expect a 412 on failure): - -``` -use Guzzle\Http\Exception\ClientErrorResponseException; - -try { - $oldImmutableFile = $container->getObject('payroll_2001.xlsx', array( - 'If-Unmodified-Since' => 'Mon, 31 Dec 2001 23:00:00 GMT' - )); -} catch (ClientErrorResponseException $e) { - echo 'This file has been modified...'; -} -``` - -Finally, you can specify a range - which will return a subset of bytes from the file specified. To return the last 20B -of a file: - -```php -$snippet = $container->getObject('output.log', array('range' => 'bytes=-20')); -``` - -## Update an existing object - -Updating content is easy: - -```php -$file->setContent(fopen('/path/to/new/content', 'r+')); -$file->update(); -``` - -Bear in mind that updating a file name will result in a new file being generated (under the new name). You will need to -delete the old file. - -## Copy object - -To copy a file to another location, you need to specify a string-based destination path: - -```php -$object->copy('/container_2/new_object_name'); -``` - -## Delete object - -```php -$object->delete(); -``` - -## Get object metadata - -Metadata will be automatically populated when the file is retrieved from the API. To access these populated values: - -```php -$object->getMetadata(); -``` - -But, if for some reason, you'd like to retrieve the freshest data by executing a HTTP request to the API: - -```php -$object->retrieveMetadata(); -``` - -## Update object metadata - -Similarly, with setting metadata there are two options: you can update the metadata values of the local object (i.e. no -HTTP request) if you anticipate you'll be executing one soon (an update operation for example): - -```php -// There's no need to execute a HTTP request, because we'll soon do one anyway for the update operation -$object->setMetadata(array( - 'Author' => 'Hemingway' -)); - -// ... code here - -$object->update(); -``` - -Alternatively, you can update the API straight away - so that everything is retained: - -```php -$object->saveMetadata(array( - 'Author' => 'Hemingway' -)); -``` - -Please be aware that these methods override and wipe existing values. If you want to append values to your metadata, use -the correct method: - -```php -$metadata = $object->appendToMetadata(array( - 'Author' => 'Hemingway' -)); - -$object->saveMetadata($metadata); -``` - -## Extract archive - -CloudFiles provides you the ability to extract uploaded archives to particular destinations. The archive will be extracted -and its contents will populate the particular area specified. To upload file (which might represent a directory structure) -into a particular container: - -```php -use OpenCloud\ObjectStore\Constants\UrlType; - -$service->bulkExtract('container_1', fopen('/home/jamie/files.tar.gz','r'), UrlType::TAR_GZ); -``` - -You can also omit the container name (i.e. provide an empty string as the first argument). If you do this, the API will -create the containers necessary to house the extracted files - this is done based on the filenames inside the archive. - -## Bulk delete - -Bulk delete a set of paths: - -```php -$pathsToBeDeleted = array('/container_1/old_file', '/container_2/notes.txt', '/container_1/older_file.log'); - -$service->bulkDelete($pathsToBeDeleted); -``` +http://docs.php-opencloud.com/en/latest/services/object-store/objects.html diff --git a/rackspace/php-opencloud/docs/userguide/Queues/Claim.md b/rackspace/php-opencloud/docs/userguide/Queues/Claim.md old mode 100755 new mode 100644 index 13e12eac8..58b57d22f --- a/rackspace/php-opencloud/docs/userguide/Queues/Claim.md +++ b/rackspace/php-opencloud/docs/userguide/Queues/Claim.md @@ -1,123 +1,5 @@ ## 1. Introduction -A __Claim__ is the process of a worker checking out a message to perform a task. Claiming a message prevents other -workers from attempting to process the same messages. +Our docs have moved! Please visit the below link: -## 2. Setup - -A Claim is initialized on its parent object, a Queue: - -```php -// To intialize an empty object: -$claim = $queue->getClaim(); - -// or retrieve a specific claim: -$claim = $queue->getClaim('51db7067821e727dc24df754'); -``` - -## 3. Claim messages - -### 3.1 Description - -This operation claims a set of messages (up to the value of the limit parameter) from oldest to newest and skips any -messages that are already claimed. If no unclaimed messages are available, the API returns a `204 No Content` message. - -When a client (worker) finishes processing a message, it should delete the message before the claim expires to ensure -that the message is processed only once. As part of the delete operation, workers should specify the claim ID (which is -best done by simply using the provided href). If workers perform these actions, then if a claim simply expires, the -server can return an error and notify the worker of the race condition. This action gives the worker a chance to roll -back its own processing of the given message because another worker can claim the message and process it. - -The age given for a claim is relative to the server's clock. The claim's age is useful for determining how quickly -messages are getting processed and whether a given message's claim is about to expire. - -When a claim expires, it is released. If the original worker failed to process the message, another client worker can -then claim the message. - -### 3.2 Attributes - -The `ttl` attribute specifies how long the server waits before releasing the claim. The ttl value must be between 60 and -43200 seconds (12 hours). You must include a value for this attribute in your request. - -The `grace` attribute specifies the message grace period in seconds. The value of grace value must be between 60 and -43200 seconds (12 hours). You must include a value for this attribute in your request. To deal with workers that have -stopped responding (for up to 1209600 seconds or 14 days, including claim lifetime), the server extends the lifetime of -claimed messages to be at least as long as the lifetime of the claim itself, plus the specified grace period. If a -claimed message would normally live longer than the grace period, its expiration is not adjusted. - -The `limit` attribute specifies the number of messages to return, up to 20 messages. If limit is not specified, limit - defaults to 10. The limit parameter is optional. - -### 3.3 Code - -```php -use OpenCloud\Common\Constants\Datetime; - -$queue->claimMessages(array( - 'limit' => 15, - 'grace' => 5 * Datetime::MINUTE, - 'ttl' => 5 * Datetime::MINUTE -)); -``` - -## 4. Query claim - -### 4.1 Description - -This operation queries the specified claim for the specified queue. Claims with malformed IDs or claims that are not -found by ID are ignored. - -### 4.2 Attributes - -Claim ID. - -### 4.3 Code - -```php -$claim = $queue->getClaim('51db7067821e727dc24df754'); -``` - -## 5. Update claim - -### 5.1 Description - -This operation updates the specified claim for the specified queue. Claims with malformed IDs or claims that are not -found by ID are ignored. - -Clients should periodically renew claims during long-running batches of work to avoid losing a claim while processing a -message. The client can renew a claim by executing this method on a specific __Claim__ and including a new TTL. The API - will then reset the age of the claim and apply the new TTL. - -### 5.2 Attributes - -See section 4.2. - -### 5.3 Code - -```php -use OpenCloud\Common\Constants\Datetime; - -$claim->update(array( - 'ttl' => 10 * Datetime::MINUTE -)); -``` - -## 6. Release claim - -### 6.1 Description - -This operation immediately releases a claim, making any remaining undeleted messages that are associated with the -claim available to other workers. Claims with malformed IDs or claims that are not found by ID are ignored. - -This operation is useful when a worker is performing a graceful shutdown, fails to process one or more messages, or is -taking longer than expected to process messages, and wants to make the remainder of the messages available to other workers. - -### 6.2 Attributes - -See section 4.2. - -### 6.3 Code - -```php -$message->delete(); -``` \ No newline at end of file +http://docs.php-opencloud.com/en/latest/services/queues/claims.html diff --git a/rackspace/php-opencloud/docs/userguide/Queues/Message.md b/rackspace/php-opencloud/docs/userguide/Queues/Message.md old mode 100755 new mode 100644 index 2ce328cbc..767bb0b1d --- a/rackspace/php-opencloud/docs/userguide/Queues/Message.md +++ b/rackspace/php-opencloud/docs/userguide/Queues/Message.md @@ -1,219 +1,5 @@ ## 1. Introduction -A __Message__ is a task, a notification, or any meaningful data that a producer or publisher sends to the queue. A -message exists until it is deleted by a recipient or automatically by the system based on a TTL (time-to-live) value. +Our docs have moved! Please visit the below link: -## 2. Setup - -A message is initialized from its parent object, a Queue: - -```php -// Setup an empty object -$message = $queue->getMessage(); - -// or retrieve an existing one -$message = $queue->getMessage(''); -``` - -## 3. Post message - -### 3.1 Description - -This operation posts the specified message or messages. You can submit up to 10 messages in a single request. - -When posting new messages, you specify only the `body` and `ttl` for the message. The API will insert metadata, such as -ID and age. - -### 3.2 Parameters - -How you pass through the array structure depends on whether you are executing multiple (3.3.2) or single (3.3.3) posts, -but the keys are the same. - -The `body` attribute specifies an arbitrary document that constitutes the body of the message being sent. The size of -this body is limited to 256 KB, excluding whitespace. - -The `ttl` attribute specifies how long the server waits before marking the message as expired and removing it from the -queue. The value of ttl must be between 60 and 1209600 seconds (14 days). Note that the server might not actually -delete the message until its age has reached up to (ttl + 60) seconds, to allow for flexibility in storage implementations. - -### 3.3 Code samples - -#### 3.3.1 Posting a single message - -``` -use OpenCloud\Common\Constants\Datetime; - -$queue->createMessage(array( - 'body' => (object) array( - 'event' => 'BackupStarted', - 'deadline' => '26.12.2013 - ), - 'ttl' => 2 * Datetime::DAY -)); -``` - -#### 3.3.2 Post a batch of messages - -Please note that the list of messages will be truncated at 10. For more, please execute another method call. - -```php -use OpenCloud\Common\Constants\Datetime; - -$messages = array( - array( - 'body' => (object) array( - 'play' => 'football' - ), - 'ttl' => 2 * Datetime::DAY - ), - array( - 'body' => (object) array( - 'play' => 'tennis' - ), - 'ttl' => 50 * Datetime::HOUR - ) -); - -$queue->createMessages($messages); -``` - -## 4. Get messages - -### 4.1 Description - -This operation gets the message or messages in the specified queue. - -Message IDs and markers are opaque strings. Clients should make no assumptions about their format or length. Furthermore, -clients should assume that there is no relationship between markers and message IDs (that is, one cannot be derived -from the other). This allows for a wide variety of storage driver implementations. - -Results are ordered by age, oldest message first. - -### 4.2 Parameters - -A hash of options. - -|Name|Style|Type|Description| -|----|-----|----|-----------| -|marker|Query|​String|Specifies an opaque string that the client can use to request the next batch of messages. The marker -parameter communicates to the server which messages the client has already received. If you do not specify a value, the -API returns all messages at the head of the queue (up to the limit). Optional.| -|limit|Query|​Integer|When more messages are available than can be returned in a single request, the client can pick up -the next batch of messages by simply using the URI template parameters returned from the previous call in the "next" field. -Specifies up to 10 messages (the default value) to return. If you do not specify a value for the limit parameter, the -default value of 10 is used. Optional.| -|echo|Query|​Boolean|Determines whether the API returns a client's own messages. The echo parameter is a Boolean value -(true or false) that determines whether the API returns a client's own messages, as determined by the uuid portion of -the User-Agent header. If you do not specify a value, echo uses the default value of false. If you are experimenting -with the API, you might want to set echo=true in order to see the messages that you posted. The echo parameter is optional. -|include_claimed|Query|​Boolean|Determines whether the API returns claimed messages and unclaimed messages. The -include_claimed parameter is a Boolean value (true or false) that determines whether the API returns claimed messages -and unclaimed messages. If you do not specify a value, include_claimed uses the default value of false -(only unclaimed messages are returned). Optional. -|----|-----|----|-----------| - -### 4.3 Code sample - -```php -$messages = $queue->listMessages(array( - 'marker' => '51db6f78c508f17ddc924357', - 'limit' => 20, - 'echo' => true -)); - -while ($message = $messages->next()) { - echo $message->getId() . PHP_EOL; -} -``` - -## 5. Get a set of messages by ID - -### 5.1 Description - -This operation provides a more efficient way to query multiple messages compared to using a series of individual GET. -Note that the list of IDs cannot exceed 20. If a malformed ID or a nonexistent message ID is provided, it is ignored, -and the remaining messages are returned. - -### 5.2 Parameters - -A hash of options. - -|Name|Style|Type|Description| -|----|-----|----|-----------| -|ids|Query|String|Specifies the IDs of the messages to get. Format multiple message ID values by separating them with -commas (comma-separated). Optional.| -|claim_id|Query|​String|Specifies the claim ID with which the message is associated. Optional.| -|----|-----|----|-----------| - -### 5.3 Code sample - -```php -$ids = array('51db6f78c508f17ddc924357', 'f5b8c8a7c62b0150b68a50d6'); - -$messages = $queue->listMessages(array('ids' => $ids)); - -while ($message = $messages->next()) { - echo $message->getId() . PHP_EOL; -} -``` - -## 6. Delete a set of messages by ID - -### 6.1 Description - -This operation immediately deletes the specified messages. If any of the message IDs are malformed or non-existent, -they are ignored. The remaining valid messages IDs are deleted. - -### 6.2 Parameters - -An array of IDs. - -### 6.3 Code sample - -```php -$ids = array('51db6f78c508f17ddc924357', 'f5b8c8a7c62b0150b68a50d6'); - -$response = $queue->deleteMessages($ids); -``` - -## 7. Get a specific message - -### 7.1 Description - -This operation gets the specified message from the specified queue. - -### 7.2 Parameters - -Message ID. - -### 7.3 Object properties - -`href` is an opaque relative URI that the client can use to uniquely identify a message resource and interact with it. - -`ttl` is the TTL that was set on the message when it was posted. The message expires after (ttl - age) seconds. - -`age` is the number of seconds relative to the server's clock. - -`body` is the arbitrary document that was submitted with the original request to post the message. - -### 7.4 Code sample - -```php -$message = $queue->getMessage('51db6f78c508f17ddc924357'); -``` - -## 8. Delete message - -### 8.1 Description - -This operation immediately deletes the specified message. - -### 8.2 Parameters - -None. - -### 8.3 Code sample - -```php -$message->delete(); -``` \ No newline at end of file +http://docs.php-opencloud.com/en/latest/services/queues/messages.html diff --git a/rackspace/php-opencloud/docs/userguide/Queues/Queue.md b/rackspace/php-opencloud/docs/userguide/Queues/Queue.md old mode 100755 new mode 100644 index 16290f445..2299a72e2 --- a/rackspace/php-opencloud/docs/userguide/Queues/Queue.md +++ b/rackspace/php-opencloud/docs/userguide/Queues/Queue.md @@ -1,156 +1,5 @@ ## 1. Introduction -A Queue is an entity that holds messages. Ideally, a queue is created per work type. For example, if you want to -compress files, you would create a queue dedicated to this job. Any application that reads from this queue would only -compress files. +Our docs have moved! Please visit the below link: -## 2. Setup - -```php -$service = $client->queuesService('cloudQueues', 'ORD'); -``` - -## 3. Client IDs - -With most of Marconi's operation, you must specify a __Client ID__ which will be used as a unique identifier for the -process accessing this Queue. This is basically a UUID that must be unique to each client accessing the API - it can be -an arbitrary string. - -```php -$service->setClientId(); - -echo $service->getClientId(); -``` - -If you call `setClientId` without any parameters, a UUID is automatically generated for you. - -## 4. List queues - -### 4.1 Description - -This operation lists queues for the project. The queues are sorted alphabetically by name. - -### 4.2 Parameters - -|Name|Style|Type|Description| -|----|-----|----|-----------| -|marker|Query|​String|Specifies the name of the last queue received in a previous request, or none to get the first page - of results. Optional.| -|limit|Query|Integer|Specifies the number of queues to return. The default value for the number of queues returned is -10. If you do not specify this parameter, the default number of queues is returned. Optional.| -|detailed|Query|​Boolean|Determines whether queue metadata is included in the response. The default value for this - parameter is false, which excludes the metadata. Optional.| - |----|-----|----|-----------| - -### 4.3 Code sample - -```php -$queues = $service->listQueues(); - -while ($queue = $queues->next()) { - echo $queue->getName() . PHP_EOL; -} -``` - -## 5. Create queue - -### 5.1 Description - -This operation creates a new queue. - -### 5.2 Parameters - -A string representation of the name for your new Queue. The name must not exceed 64 bytes in length, and it is limited -to US-ASCII letters, digits, underscores, and hyphens. - -### 5.3 Code sample - -```php -$queue = $service->createQueue('new_queue'); -``` - -## 6. Retrieve queue - -### 6.1 Description - -Returns a `Queue` object for use. - -### 6.2 Parameters - -Queue name. - -### 6.3 Code sample - -```php -$queue = $service->getQueue('new_queue'); -``` - -## 7. Check queue existence - -### 7.1 Description - -This operation verifies whether the specified queue exists by returning `TRUE` or `FALSE`. - -### 7.2 Parameters - -### 7.3 Code sample - -```php -if ($service->hasQueue('new_queue')) { - // do something -} -``` - -## 8. Update queue metadata (permanently to the API) - -### 4.1 Description - -This operation replaces any existing metadata document in its entirety. Ensure that you do not accidentally overwrite -existing metadata that you want to retain. If you want to _append_ metadata, ensure you merge a new array to the -existing values. - -### 4.2 Parameters - -Hash of key pairs. - -### 4.3 Code sample - -```php -$queue->saveMetadata(array( - 'foo' => 'bar' -)); -``` - -## 9. Retrieve the queue metadata (fresh from the API) - -### 4.1 Description - -This operation returns metadata, such as message TTL, for the queue. - -### 4.2 Parameters - -None. - -### 4.3 Code sample - -```php -$metadata = $queue->retrieveMetadata(); - -print_r($metadata->toArray()); -``` - -## 10. Get queue stats - -### 4.1 Description - -This operation returns queue statistics, including how many messages are in the queue, categorized by status. - -### 4.2 Parameters - -None. - -### 4.3 Code sample - -```php -$queue->getStats(); -``` \ No newline at end of file +http://docs.php-opencloud.com/en/latest/services/queues/queues.html diff --git a/rackspace/php-opencloud/docs/userguide/accessip.md b/rackspace/php-opencloud/docs/userguide/accessip.md old mode 100755 new mode 100644 index bb5f38ee6..9d343de11 --- a/rackspace/php-opencloud/docs/userguide/accessip.md +++ b/rackspace/php-opencloud/docs/userguide/accessip.md @@ -1,52 +1,6 @@ About the Access IP Addresses ============================= -OpenStack deployments generally provide new [servers](servers.md) with one -or two network interfaces, each with its own address(es). Usually, one of -these will be a public interface, with its addresses available on the Internet. +Our docs have moved! Please visit the below link: -However, in some cases, the servers are created on an internal network -(this is especially common in a hybrid solution where physical and virtual -servers are intermixed). The servers may be behind a NAT device, firewall, -or other network device that prohibits direct access to the server itself. - -The `Server` object provides two attributes that are used to instruct -applications what IP address to use. These are called the *access IP* -addresses, and they are, in essence, documentation strings used to -direct applications to the correct network address. They can be changed -at will by the server's owner, and OpenStack Nova does not perform any -validation on them: - -* `accessIPv4` holds the IPv4 access address, and -* `accessIPv6` holds the IPv6 access address. - -### Updating the access IP address(es) - -For example, you may have a private cloud with internal addresses in the -10.1.x range. However, you can access a server via a firewall device at -address 50.57.94.244. In this case, you can change the `accessIPv4` attribute -to point to the firewall: - - $compute = $cloud->Compute(); - $server = $compute->Server('908c5617-26c2-4535-99a9-3f20e4b74835'); - $server->Update(array('accessIPv4'=>'50.57.94.244')); - -When a client application retrieves the server's information, it will know -that it needs to use the `accessIPv4` address to connect to the server, and -*not* the IP address assigned to one of the network interfaces. - -### Retrieving the server's IP address - -The `Server::ip()` method is used to retrieve the server's IP address. -It has one optional parameter: the format (either IPv4 or IPv6) of the address -to return (by default, it returns the IPv4 address): - - $server = $compute->Server('...'); // get a server - print $server->ip(); // the IPv4 address - print $server->ip(4); // the IPv4 address, too - print $server->ip(6); // the IPv6 address - print $server->ip(8); // causes an exception - -## What next? - -Return to the [Table of Contents](toc.md) +http://docs.php-opencloud.com/en/latest/services/compute/servers.html diff --git a/rackspace/php-opencloud/docs/userguide/caching-credentials.md b/rackspace/php-opencloud/docs/userguide/caching-credentials.md index d38d69937..949dcad53 100644 --- a/rackspace/php-opencloud/docs/userguide/caching-credentials.md +++ b/rackspace/php-opencloud/docs/userguide/caching-credentials.md @@ -1,39 +1,5 @@ # Caching credentials -You can speed up your API operations by caching your credentials in a (semi-)permanent location, such as your -DB or local filesystem. This enable subsequent requests to access a shared resource, instead of repetitively having to -re-authenticate on every thread of execution. +Our docs have moved! Please visit the below link: -Tokens are valid for 24 hours, so you can effectively re-use the same cached value for that period. If you try to use -a cached version that has expired, an authentication request will be made. - -## Filesystem example - -In this example, credentials will be saved to a file in the local filesystem. Be sure to exclude it from your VCS. - -```php -use OpenCloud\Rackspace; - -$client = new Rackspace(Rackspace::US_IDENTITY_ENDPOINT, array( - 'username' => 'foo', - 'apiKey' => 'bar' -)); - -$cacheFile = __DIR__ . '/.opencloud_token'; - -// If the cache file exists, try importing it into the client -if (file_exists($cacheFile)) { - $data = unserialize(file_get_contents($cacheFile)); - $client->importCredentials($data); -} - -$token = $client->getTokenObject(); - -// If no token exists, or the current token is expired, re-authenticate and save the new token to disk -if (!$token || ($token && $token->hasExpired())) { - $client->authenticate(); - file_put_contents($cacheFile, serialize($client->exportCredentials())); -} -``` - -In tests, the above code shaved about 1-2s off the execution time. \ No newline at end of file +http://docs.php-opencloud.com/en/latest/caching-creds.html diff --git a/rackspace/php-opencloud/docs/userguide/dbaas.md b/rackspace/php-opencloud/docs/userguide/dbaas.md old mode 100755 new mode 100644 index c5c74f754..fe0327861 --- a/rackspace/php-opencloud/docs/userguide/dbaas.md +++ b/rackspace/php-opencloud/docs/userguide/dbaas.md @@ -1,186 +1,6 @@ Working with Cloud Databases ============================ -Cloud Databases is a "database-as-a-service" product offered by Rackspace. Since it is -not an official OpenStack project, it is only available via Rackspace connections, -and *not* through an OpenStack connection. +Our docs have moved! Please visit the below link: -Therefore, this example will cause an error: - - $cloud = new \OpenCloud\OpenStack(...); - $dbservice = $cloud->DbService(...); // this won't work - -However, this code *will* work properly: - - $cloud = new \OpenCloud\Rackspace(...); - $dbservice = $cloud->DbService(...); // this will - -Like other [services](services.md), you connect to a specific deployment of Cloud -Databases by supplying the service name, region, and URL type: - - $dbservice = $cloud->DbService('cloudDatabases', 'ORD', 'publicURL'); - -and, like the other services, you can rely upon the defaults to select the proper one: - - $dbservice = $cloud->DbService(NULL, 'LON'); - -## Instances and databases - -An `Instance` is a virtual server running MySQL; it can support multiple databases. -As such, it has much in common with the [Server](servers.md) object. However, there -are some differences between the `Server` and the `Instance` objects: - -1. Like a Server, a database Instance requires a [Flavor object](flavors.md) at creation. This specifies the amount of RAM allocated to the Instance. -1. Unlike a Server, the Instance's Flavor object does not control the disk space; this is set via the volume size attribute. -1. Unlike a Server, you do not specify an [Image object](images.md). The image is provided by the database service. -1. Listing the available Flavor objects for the DbService service is the same as for the Compute service. - -Listing flavors: - - $compute = $cloud->Compute(); - $dbaas = $cloud->DbService(); - $compute_flavors = $compute->FlavorList(); - $dbaas_flavors = $dbaas->FlavorList(); - -The flavors themselves *are* different between the Compute service and the DbService, -however. In other words, you cannot use a `Flavor` from a Compute service to create -a database Instance, and you cannot use a `Flavor` from a DbService service to -create a Server object. - -### Create a new DbService Instance - - $dbaas = $cloud->DbService(); - $inst = $dbaas->Instance(); // new, empty Instance - $inst->flavor = $dbaas->Flavor(2); // flavor ID - $inst->volume->size = 4; // this specifies 4GB of disk - $inst->Create(); // this actually creates the instance - -### Deleting an instance - -This is very simple: - - $inst->Delete(); - -### Performing actions on an instance - -Like a Server, you can reboot (called *Restart* in DbService terminology) or resize -an instance. - -#### Restarting an instance - - $inst = $dbaas->Instance({instance-id}); - $inst->Restart(); - -#### Resizing an instance - -There are two methods for resizing an instance. The first, `Resize()`, changes the amount -of RAM allocated to the instance: - - $inst->Resize($dbaas->Flavor(4)); // uses the larger flavor - -You can also independently change the volume size to increase the disk space: - - $inst->ResizeVolume(8); // increase to 8GB of disk - -## Databases and Users - -Instances can have multiple databases; a `Database` object corresponds to a single -MySQL database. - -### Creating a new database - -To create a new database, you must supply it with a name; you can optionally -specify its character set and collating sequence: - - $db = $instance->Database(); // new, empty database object - $db->Create(array('name'=>'simple')); // creates the database w/defaults - $prod = $instance->Database(); // empty database - $prod->Create(array( - 'name' => 'production', - 'character_set' => 'utf8', // specify the character set - 'collate' => 'utf8_general_ci' // specify sort/collate sequence - )); - -You can find values for `character_set` and `collate` at -[the MySQL website](http://dev.mysql.com/doc/refman/5.0/en/charset-mysql.html). - -### Deleting a database - -This is also a simple operation: - - $db = $instance->Database('simple'); // named database object - $db->Delete(); - -Note that this is destructive; all your data will be wiped away and will not be -retrievable. - -### Listing databases - -The `DatabaseList` object is a `Collection` of databases in an instance: - - $dblist = $instance->DatabaseList(); - while($db = $dblist->Next()) - printf("Database: %s\n", $db->name); - -### Creating users - -Database users exist at the `Instance` level, but can be associated with a specific -`Database`. They are represented by the `User` object, which is constructed by -the `User()` factory method: - - $instance = $dbaas->Instance(); - $user = $instance->User(); // a new, empty user - -Users cannot be altered after they are created, so they must be assigned to -databases when they are created: - - $user->name = 'Fred'; // the user must have a name - $user->password = 'S0m3thyng'; - $user->AddDatabase('simple'); // Fred can access the 'simple' DB - $user->AddDatabase('production'); // as well as 'production' - $user->Create(); // creates the user - -If you need to add a new database to a user after it's been created, you must -delete the user and then re-add it. - -As a shortcut, you can specify all the info in the parameter of the `Create()` method: - - $user = $instance->User(); - $user->Create(array( - 'name' => 'Fred', - 'password' => 'S0m3thyng', // I made this up; don't bother trying it - 'databases => array('simple','production'))); - -### Deleting users - -This should be familiar to you by now: - - $user->Delete(); - -## The root user - -By default, Cloud Databases does not enable the root user. In most cases, the root -user is not needed, and having one can leave you open to security violations. However, -if you want to enable access to the root user, the `EnableRootUser()` method is -available: - - $root_user = $instance->EnableRootUser(); - -This returns a regular `User` object with the `name` attribute set to `root` and the -`password` attribute set to an auto-generated password. - -To check if the root user is enabled, use the `IsRootEnabled()` method: - - if ($instance->IsRootEnabled()) - print("The root user is enabled\n"); - else - print("No root access to this instance\n"); - -## Sample code - -The directory `samples/database` contains various tested sample code snippets for your -use. - -## What next? - -Return to the [Table of Contents](toc.md) +http://docs.php-opencloud.com/en/latest/services/database/index.html diff --git a/rackspace/php-opencloud/docs/userguide/flavors.md b/rackspace/php-opencloud/docs/userguide/flavors.md old mode 100755 new mode 100644 index aa6d06afc..4bf978b52 --- a/rackspace/php-opencloud/docs/userguide/flavors.md +++ b/rackspace/php-opencloud/docs/userguide/flavors.md @@ -1,72 +1,6 @@ Working with Flavors ==================== -A *flavor* is a named definition of certain server parameters such as the -amount of RAM and disk space available. (There are other parameters set via -the flavor, such as the amount of disk space and the number of virtual CPUs, -but a discussion of those is too in-depth for a simple Getting Started Guide -like this one.) +Our docs have moved! Please visit the below link: -A `Flavor` object is generated from the `Flavor()` -method on the `Compute` object: - - $flavor = $compute->Flavor(); - -This is an empty flavor, and not very useful by itself. Normally, you'll retrieve -a flavor by its ID: - - $flavor = $compute->Flavor(2); - -The ID can be either a full UUID or simply an integer (as shown above). The -actual format will depend upon your cloud provider. - -A list of flavors is provided by the `FlavorList` [Collection](collections.md) object, -which is generated by the `FlavorList()` method: - - $flavors = $compute->FlavorList(); - while($flavor = $flavors->Next()) - printf("Flavor %s has %dMB of RAM and %dGB of disk\n", - $flavor->name, $flavor->ram, $flavor->disk); - -### Flavor details - -By default, the `FlavorList()` method returns full details on all flavors. -However, because of the overhead involved in retrieving all the details, -this function can be slower than expected. You can supply an optional -boolean parameter to the `FlavorList()` method to determine whether or not -the flavor details are included: - - $fastlist = $compute->FlavorList(FALSE); // name + id only - $slowlist = $compute->FlavorList(TRUE); // include all details - -### Filtering flavors - -The (optional) second parameter to the `FlavorList()` method is an -associative array of filter parameters. See the -[List Flavors API call](http://docs.rackspace.com/servers/api/v2/cs-devguide/content/List_Flavors-d1e4188.html) -in the Next Generation Cloud Servers Developer Guide for a list of the available -parameters. - -For example, you may be only interested in flavors that have at least 4GB of -memory: - - $biglist = $compute->FlavorList(TRUE, array('minRam'=>4096)); - -Or perhaps only flavors that have at least 200GB of disk: - - $biglist = $compute->FlavorList(TRUE, array('minDisk'=>200)); - -These filters can, of course, be combined: - - $mylist = $compute->FlavorList( - TRUE, - array('minRam'=>4000, 'minDisk'=>200)); - -### Examples - -The file `samples/compute/flavors.php` has some examples of working with -flavors. - -## What next? - -Return to the [Table of Contents](toc.md) +http://docs.php-opencloud.com/en/latest/services/compute/flavors.html diff --git a/rackspace/php-opencloud/docs/userguide/networks.md b/rackspace/php-opencloud/docs/userguide/networks.md old mode 100755 new mode 100644 index 0d566d295..f6a153508 --- a/rackspace/php-opencloud/docs/userguide/networks.md +++ b/rackspace/php-opencloud/docs/userguide/networks.md @@ -1,123 +1,6 @@ Working with Cloud Networks =========================== -Rackspace Cloud Networks is a virtual "isolated network" product -based upon (though not strictly identical to) the [OpenStack -Quantum](http://quantum.openstack.org) project. With Cloud Networks, -you can create multiple isolated networks and associate them with -new Cloud Servers. (You cannot add an isolated network to an -existing Cloud Server at this time, although that feature may be -available in the future.) +Our docs have moved! Please visit the below link: -Since the network is a feature of the Cloud Servers product, you -use the `Compute::network()` method to create a new network, and -the `Compute::networkList()` method to retrieve information about -existing networks. - -### Pseudo-networks - -Rackspace has two *pseudo-networks* called `public` and `private`. -The `public` network represents the Internet, while the `private` -network represents the Rackspace internal ServiceNet (an infrastructure -network used to facilitate communication within a Rackspace data -center). These are called "pseudo-networks" because they are not -actually represented in the Quantum component, but have a special -representation that you must use if you want to associate your -server with them. - -The `public` network is represented by the special UUID -`00000000-0000-0000-0000-000000000000` and the `private` network -by `11111111-1111-1111-1111-111111111111`. php-opencloud -provides the special constants `RAX_PUBLIC` and `RAX_PRIVATE` to -make using these networks easier (see [Create a server with an -isolated network](#server) below). - - -### Create a network - -A Cloud Network is identified by a *label* and must be defined with -a network *CIDR* address range. For example, we can create a network -used by our backend servers on the 192.168.0.0 network like this: - - // assume $compute is our Compute service - $backend = $compute->network(); // new, empty network object - $backend->create(array( - 'label' => 'Backend Network', // label it - 'cidr' => '192.168.0.0/16' // this provides the address range - )); - -The ID of the network is now available in `$backend->id`. - - -### Delete a network - -To delete a network, use the `delete()` method: - - $backend->delete(); - -Note that you cannot delete a network if there are still servers -attached to it. - -### Listing networks - -The `Compute::networkList()` method returns a [Collection](collections.md) of -`Network` objects: - - $list = $compute->networkList(); - $list->sort('label'); - while($network = $list->next()) - printf("%s: %s\n", $network->id, $network->label); - -This displays the ID and label of all your available networks -(including the `public` and `private` pseudo-networks). - - -## Create a server with an isolated network - -When you create a new server, you can specify the networks to which -it is attached via the `networks` attribute in the `Server::create()` -method. - -* If you create the server without specifying its `networks`, then it is created - using the default `public` and `private` networks. -* If you create the server and specify `networks`, it is created with *only* the - networks that you specify. That is, if you specify `networks`, then the - `public` and `private` networks are *not* attached to the server unless you - explicitly include them. - -Example: - - use OpenCloud\Compute\Constants\Network; - - // assuming the $backend network we created above - $server = $compute->server(); // create an empty server object - $server->create(array( - 'name' => 'My Server', - 'flavor' => $compute->flavor(2), - 'image' => $compute->image('c195ef3b-9195-4474-b6f7-16e5bd86acd0'), - 'networks' => array( // associate our networks - $backend, - $compute->network(Network::RAX_PUBLIC)))); - -In this example, the server `$server` is attached to the `$backend` -network that we created in the previous example. It is also attached -to the Rackspace `public` network (the Internet). However, it is -*not* attached to the Rackspace `private` network (ServiceNet). - -The `networks` attribute is an array of `Network` objects (and not -just a UUID of the network). This example will fail: - - $server->create(array( - ... - 'networks' => array(Network::RAX_PUBLIC, Network::RAX_PRIVATE))); - -because `Network::RAX_PUBLIC` and `Network::RAX_PRIVATE` are UUIDs, not networks. Use -the `Compute::network()` method to convert a UUID into a network: - - $server->create(array( - ... - 'networks' => array($compute->network(Network::RAX_PUBLIC),...))); - -## What next? - -Return to the [Table of Contents](toc.md). +http://docs.php-opencloud.com/en/latest/services/networking/index.html diff --git a/rackspace/php-opencloud/docs/userguide/servers.md b/rackspace/php-opencloud/docs/userguide/servers.md old mode 100755 new mode 100644 index 1855a588d..1ff28fd30 --- a/rackspace/php-opencloud/docs/userguide/servers.md +++ b/rackspace/php-opencloud/docs/userguide/servers.md @@ -1,181 +1,6 @@ Working with Servers ==================== -A *server* is a virtual machine instance that is managed by OpenStack Nova, -the `Compute` service. One advantage of using OpenStack is that the -virtualization layer makes it easy to create and discard servers as needed. - -The `Server` object, generated by the `Compute::server()` method, is used -to work with servers. You can create, update, or delete servers, and you can -perform various *actions* on the server, such as creating a backup image, -resizing it, or rebooting it. - -To create an empty server: - - $serv = $compute->server(); // assumes that $compute exists - -To retrieve the data on an existing server: - - $serv = $compute->server('9bfd203a-0695-410d-99a9-66c4194c967b'); - -## Creating, updating, or deleting a server - -### Creating a new server - -A server requires both a [Flavor object](flavors.md) and an -[Image object](images.md) to -be created. In addition, a server requires a name. You can easily create a -new server by setting the proper values and calling the `create()` method: - - $server = $compute->server(); - $server->name = 'Small Server'; - $server->flavor = $compute->flavor(2); - $server->image = $compute->image('c195ef3b-9195-4474-b6f7-16e5bd86acd0'); - $server->create(); - -As a shortcut, you can also pass these parameters to the `create()` method -in an associative array: - - $server = $compute->server(); - $server->create(array( - 'name' => 'Small Server', - 'flavor' => $compute->Flavor(2), - 'image' => $compute->Image('c195ef3b-9195-4474-b6f7-16e5bd86acd0') - )); - -Note that when the `create()` request completes, the server is not actually -created. Instead, the completion of the request indicates that the server -build has been initiated in the Compute instance. Server builds typically -take 1-5 minutes to complete (depending upon the size of the server). However, -the initial response will return the server's ID as well as the assigned -root password: - - // assuming we've created $server, above - printf("ID is %s, root password is %s\n", - $server->id, $server->adminPass); - -(Note: it is not recommended that you print out the root password because of -security risks. This is only provided as an example.) - -When you create a new server on the Rackspace public cloud, you can also -associate it with one or more isolated networks. For more information, see -[Working with Cloud Networks](networks.md). - -### Rebuilding an existing server - -"Rebuilding" a server is nearly identical to creating one; you must supply -an Image object. You can also change the server's name as part of the rebuild. -The primary difference between a create and a rebuild is that, in the rebuild, -the server's IP address(es) are retained (when the server is created, new IP -addresses are assigned). - -To rebuild a server: - - $server = $compute->server('abaf0...'); // existing server - $server->rebuild(array( - 'adminPass' => 'REPLACE THIS WITH THE SERVER ROOT PASSWORD', - 'name' => 'A Bigger Server', // name is not required on rebuild - 'image' => $compute->image('c195ef3b-9195-4474-b6f7-16e5bd86acd0'))); - -### Updating a server - -The `update()` method is very similar to `create()` except that the only -attributes of a server that you are permitted to update are its name and -the [access IP addresses](accessip.md). - - $compute = $cloud->compute(); - $server = $compute->server('908c5617-26c2-4535-99a9-3f20e4b74835'); - $server->update(array('accessIPv4'=>'50.57.94.244')); - -### Deleting a server - -The `delete()` method is short and sweet: it immediately initiates the -deletion of the server. Note that this is a destructive mechanism and it is -unlikely that the server can be recovered. Example: - - $compute = $cloud->compute(); - $server = $compute->server('908c5617-26c2-4535-99a9-3f20e4b74835'); - $server->delete(); // BAM! bye-bye - -## Server actions - -You can perform various actions on a server, such as rebooting it, resizing -it, or changing the root password. - -### Setting the root password - -Use the `setPassword()` method to change the root user's password: - - $server->setPassword('new password'); - -Note that it may take a few second for the new password to take effect. Also, -password restrictions (such as the minimum number of characters, numbers of -punctuation characters, and so forth) are enforced by the operating system and are -not always detectable by the Compute service. This means that, even though -the `setPassword()` method succeeds, the password may not be changed, and -there may not be any feedback to that effect. - -### To reboot the server - -You can perform either a *hard* reboot (this is like pulling the power cord -and then restarting) or a *soft* reboot (initiated by the operating system -and generally less disruptive than a hard reboot). A hard reboot is -performed by default: - - $server->reboot(); // hard reboot - $server->reboot(ServerState::REBOOT_STATE_HARD); // also a hard reboot - $server->reboot(ServerState::REBOOT_STATE_SOFT); // a soft reboot - -If the server is "hung," or unresponsive, a hard reboot may sometimes be -the only way to access the server. - -### To resize the server - -A server can be resized by providing a new [Flavor object](flavors.md): - - $server->resize($compute->flavor(5)); - -Once the resize completes (check the `$server->status`), you can either -confirm it: - - $server->resizeConfirm(); - -or revert it back to the original size: - - $server->resizeRevert(); - -### To rescue/unrescue a server - -In rescue mode, a server is rebuilt to a pristine state and the existing -filesystem is mounted so that you can edit files and diagnose issues. -See -[this document](http://docs.rackspace.com/servers/api/v2/cs-devguide/content/rescue_mode.html) -for more details. - -Put server into rescue mode: - - $password = $server->rescue(); - -The `$password` is the assigned root password of the rescue server. - -Take server out of rescue mode: - - $server->unrescue(); - -This restores the server to its original state (plus any changes you may have -made while it was in rescue mode). - -## Volumes - -See [Working with Volumes](volumes.md) for information on the -`Server::attachVolume()` -and `Server::detachVolume()` methods. - -## What next? - -* Return to the [Table of Contents](toc.md). -* See also [Working with Networks](networks.md). -* To learn about dynamic - volume creation and assignment, see - [Working with Volumes](volumes.md). +Our docs have moved! Please visit the below link: +http://docs.php-opencloud.com/en/latest/services/compute/index.html diff --git a/rackspace/php-opencloud/docs/userguide/volumes.md b/rackspace/php-opencloud/docs/userguide/volumes.md old mode 100755 new mode 100644 index 9ce36da8c..bc161e5b3 --- a/rackspace/php-opencloud/docs/userguide/volumes.md +++ b/rackspace/php-opencloud/docs/userguide/volumes.md @@ -1,194 +1,6 @@ Working with Volumes ---------------------------------------------- -Cloud Block Storage (CBS) is a dynamic volume creation and management service -built upon the OpenStack Cinder project. See http://cinder.openstack.org for -complete details about the available features and functionality. -### Connecting to Cloud Block Storage - -The top-level object for working with CBS/Cinder is the `VolumeService` -object. Like other [services](services.md), it is created from the top-level -connection object (either `OpenStack` or `Rackspace`). The correct service is -selected from the service catalog by specifying: - -* the service name (for example, 'cloudBlockStorage') -* the region (for example, 'ORD') -* the URL type (for example, 'publicURL') - -Defaults are provided for the service name and URL type. To specify a -default region, define the `RAXSDK_VOLUME_REGION` constant *before* including -the top-level Rackspace class: - - // set the default region to London - define('RAXSDK_VOLUME_REGION','LON'); - -Use the `VolumeService()` method to create a new `VolumeService` object: - - $cloud = new \OpenCloud\Rackspace(...); - $cbs = $cloud->VolumeService(); - -You can override any defaults by specifying them: - - $cbs = $cloud->VolumeService('myService','myRegion'); - -Syntax: - - {variable} = $cloud->VolumeService({servicename}, {region}, {urltype}); - -Example: - - $dallas = $cloud->VolumeService('cloudBlockStorage', 'DFW'); - -This creates a connection to the `cloudBlockStorage` service (as it is -called at Rackspace) in the `DFW` region. - -### Volume Types - -Providers may support multiple types of volumes; at Rackspace, a volume can -either be `SSD` (solid state disk: expensive, high-performance) or -`SATA` (serial attached storage: regular disks, less expensive). - -#### Listing volume types -The `VolumeTypeList` method returns a Collection of VolumeType objects: - - $vtlist = $dallas->VolumeTypeList(); - while($vt = $vtlist->Next()) - printf("%s %s\n", $vt->id, $vt->Name()); - -This lists the volume types and their IDs. - -**php-opencloud** does not permit you to create, delete, or update volume -types, since those are administrative functions. - -#### Describe a single volume type - -If you know the ID of a volume type, use the `VolumeType` method to retrieve -information on it: - - $volumetype = $dallas->VolumeType(1); - -A volume type has only three attributes: - -* `$id` the volume type identifier -* `$name` its name -* `$extra_specs` additional information for the provider - -### Working with Volumes - -Like other objects, you can create, list, and show volumes. The `Volume` method -on the VolumeService object is the primary interface. - -#### To create a volume - -To create a volume, you must specify its size (in gigabytes). All other -parameters are optional (and defaults will be provided), though providing -the volume type is recommended. - -Example: - - $myvolume = $dallas->Volume(); // an empty volume object - $response = $myvolume->Create(array( - 'size' => 200, - 'volume_type' => $dallas->VolumeType(1), - 'display_name' => 'My Volume', - 'display_description' => 'Use this for large object storage')); - -This creates a 200GB volume. Note that the `volume_type` parameter must be -a `VolumeType` object. - -#### To list volumes - -The `VolumeList()` method returns a Collection of Volume objects: - - $volumes = $dallas->VolumeList(); - $volumes->Sort('display_name'); - while($vol = $volumes->Next()) - print $vol->Name()."\n"; - -This lists all the volumes associated with your account. - -#### To get details on a single volume - -If you specify an ID on the `Volume()` method, it retrieves information on -the specified volume: - - $myvolume = $dallas->Volume('0d0f90209...'); - printf("volume size = %d\n", $myvolume->size); - -#### To delete a volume - -The `Delete()` method deletes a volume: - - $myvolume->Delete(); - -### Working with Snapshots - -A `Snapshot` captures the contents of a volume at a point in time. It can be -used, for example, as a backup point; and you can later create a volume from -the snapshot. - -#### To create a snapshot - -A `Snapshot` object is created from the Cloud Block Storage service. However, -it is associated with a volume, and you must specify a volume to create one: - - $snapshot = $dallas->Snapshot(); // empty Snapshot object - $snapshot->Create(array( - 'display_name' => 'Name that snapshot', - 'volume_id' => $volume->id)); - -#### To list snapshots - -The `SnapshotList()` method returns a Collection of Snapshot objects: - - $snaplist = $dallas->SnapshotList(); - while($snap = $snaplist->Next()) - printf("[%s] %s\n", $snap->id, $snap->Name()); - -#### To get details on a single snapshot - -To retrieve a single `Snapshot`, specify its ID on the `Snapshot()` method: - - $snapshot = $dallas->Snapshot({snapshot-id}); - -#### To delete a snapshot - -Use the `Delete()` method to remove a snapshot: - - $snapshot->Delete(); - -### Volumes and Servers - -A volume by itself is useful when it is attached to -a server so that the server can use the volume. - -#### To attach a volume to a server - -Syntax: - - $server = $compute->Server({server-id}); - $volume = $dallas->Volume({volume-id}); - $server->AttachVolume($volume, {mount-point}) - -`{server-id}` and `{volume-id}` are the IDs of the server and volume, -respectively. `{mount-point}` is the location on the server on which to -mount the volume (usually `/dev/xvhdd` or similar). You can also supply -`'auto'` as the mount point, in which case the mount point will be -automatically selected for you. `auto` is the default value for -`{mount-point}`, so you do not actually need to supply anything for that -parameter. - -Example: - - $server = $compute->Server('010d092...'); - $volume = $dallas->Volume('39d0f0...'); - $server->AttachVolume($volume); // uses the 'auto' mount point - -#### To detach a volume from a server - -Syntax: - - $server = $compute->Server({server-id}); - $volume = $dallas->Volume({volume-id}); - $server->DetachVolume($volume); +Our docs have moved! Please visit the below link: +http://docs.php-opencloud.com/en/latest/services/volume/index.html diff --git a/rackspace/php-opencloud/lib/OpenCloud/Autoscale/Resource/AbstractResource.php b/rackspace/php-opencloud/lib/OpenCloud/Autoscale/Resource/AbstractResource.php index 6e2dd6d92..24717397e 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Autoscale/Resource/AbstractResource.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Autoscale/Resource/AbstractResource.php @@ -1,11 +1,18 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Autoscale\Resource; @@ -18,35 +25,35 @@ abstract class AbstractResource extends PersistentObject { /** - * These are used to set the object used for JSON encode. - * - * @var array + * These are used to set the object used for JSON encode. + * + * @var array */ public $createKeys = array(); - + /** - * These resources are associated with this one. When this resource object + * These resources are associated with this one. When this resource object * is populated, if a key is found matching one of these array keys, it is * set as an instantiated resource object (rather than an arbitrary string * or stdClass object). - * - * @var array + * + * @var array */ public $associatedResources = array(); - + /** * Same as an associated resource, but it's instantiated as a Collection. - * - * @var array + * + * @var array */ public $associatedCollections = array(); - + /** * Creates the object which will be JSON encoded for request. - * + * * @return \stdClass */ - public function createJson() + public function createJson() { $object = new \stdClass; @@ -55,8 +62,8 @@ public function createJson() $object->$key = $value; } } - - if (!empty($this->metadata)) { + + if (count($this->metadata)) { $object->metadata = new \stdClass; foreach ($this->getMetadata()->toArray() as $key => $value) { $object->metadata->$key = $value; @@ -65,10 +72,10 @@ public function createJson() return $object; } - + /** * Creates the object which will be JSON encoded for request. - * + * * @return array */ protected function updateJson($params = array()) @@ -77,13 +84,12 @@ protected function updateJson($params = array()) foreach ($this->createKeys as $key) { $existing[$key] = $this->$key; } - + return $existing + $params; } - + public function primaryKeyField() { return 'id'; } - -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Autoscale/Resource/Group.php b/rackspace/php-opencloud/lib/OpenCloud/Autoscale/Resource/Group.php index ea5ea3990..42737c005 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Autoscale/Resource/Group.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Autoscale/Resource/Group.php @@ -1,11 +1,18 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Autoscale\Resource; @@ -15,18 +22,18 @@ /** * An autoscaling group is monitored by Rackspace CloudMonitoring. When - * Monitoring triggers an alarm for high utilization within the autoscaling - * group, a webhook is triggered. The webhook stimulates the autoscale service - * which consults a policy in accordance with the webhook. The policy determines - * how many additional cloud servers should be added or removed in accordance + * Monitoring triggers an alarm for high utilization within the autoscaling + * group, a webhook is triggered. The webhook stimulates the autoscale service + * which consults a policy in accordance with the webhook. The policy determines + * how many additional cloud servers should be added or removed in accordance * with the alarm. - * + * * There are three components to Autoscale: - * + * * - The Scaling Group Configuration ($this->groupConfiguration) * - The Scaling Group's Launch Configuration ($this->launchConfiguration) * - The Scaling Group's Policies ($this->scalingPolicies) - * + * * @link https://github.com/rackerlabs/otter/blob/master/doc/getting_started.rst * @link http://docs.autoscale.apiary.io/ */ @@ -39,17 +46,17 @@ class Group extends AbstractResource private $scalingPolicies; private $name; protected $metadata; - + private $active; private $activeCapacity; private $pendingCapacity; private $desiredCapacity; private $paused; - + protected static $json_name = 'group'; protected static $url_resource = 'groups'; protected static $json_collection_name = 'groups'; - + /** * {@inheritDoc} */ @@ -58,23 +65,23 @@ class Group extends AbstractResource 'launchConfiguration', 'scalingPolicies' ); - + /** * {@inheritDoc} */ public $associatedResources = array( 'groupConfiguration' => 'GroupConfiguration', 'launchConfiguration' => 'LaunchConfiguration', - + ); - + /** * {@inheritDoc} */ public $associatedCollections = array( 'scalingPolicies' => 'ScalingPolicy' ); - + /** * {@inheritDoc} */ @@ -82,12 +89,12 @@ public function update($params = array()) { return $this->noUpdate(); } - + /** - * Get the current state of the scaling group, including the current set of - * active entities, the number of pending entities, and the desired number + * Get the current state of the scaling group, including the current set of + * active entities, the number of pending entities, and the desired number * of entities. - * + * * @return object|boolean * @throws Exceptions\HttpError * @throws Exceptions\ServerActionError @@ -103,10 +110,10 @@ public function getState() return (!empty($body->group)) ? $body->group : false; } - + /** * Get the group configuration for this autoscale group. - * + * * @return GroupConfiguration */ public function getGroupConfig() @@ -114,18 +121,19 @@ public function getGroupConfig() if (($config = $this->getProperty('groupConfiguration')) instanceof GroupConfiguration) { return $config; } - + $config = $this->getService()->resource('GroupConfiguration'); $config->setParent($this); if ($this->getId()) { $config->refresh(null, $config->url()); } + return $config; } - + /** * Get the launch configuration for this autoscale group. - * + * * @return LaunchConfiguration */ public function getLaunchConfig() @@ -133,38 +141,39 @@ public function getLaunchConfig() if (($config = $this->getProperty('launchConfiguration')) instanceof LaunchConfiguration) { return $config; } - + $config = $this->getService()->resource('LaunchConfiguration'); $config->setParent($this); if ($this->getId()) { $config->refresh(null, $config->url()); } + return $config; } - + /** * NB: NOT SUPPORTED YET. - * + * * @codeCoverageIgnore */ public function pause() { return $this->getService()->getClient()->post($this->url('pause'))->send(); } - + /** * NB: NOT SUPPORTED YET. - * + * * @codeCoverageIgnore */ public function resume() { return $this->getService()->getClient()->post($this->url('resume'))->send(); } - + /** * Get the scaling policies associated with this autoscale group. - * + * * @return Collection */ public function getScalingPolicies($override = false) @@ -172,12 +181,13 @@ public function getScalingPolicies($override = false) if (null === $this->scalingPolicies || $override === true) { $this->scalingPolicies = $this->getService()->resourceList('ScalingPolicy', null, $this); } + return $this->scalingPolicies; } - + /** * Get a particular scaling policy for this autoscale group. - * + * * @param object|int $id * @return ScalingPolicy */ @@ -188,7 +198,21 @@ public function getScalingPolicy($id = null) if ($id) { $config->populate($id); } + return $config; } - -} \ No newline at end of file + + public function createScalingPolicies(array $policies) + { + $url = clone $this->getUrl(); + $url->addPath('policies'); + + $body = json_encode($policies); + $this->checkJsonError(); + + return $this->getService() + ->getClient() + ->post($url, self::getJsonHeader(), $body) + ->send(); + } +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Autoscale/Resource/GroupConfiguration.php b/rackspace/php-opencloud/lib/OpenCloud/Autoscale/Resource/GroupConfiguration.php index 86df073f4..cd423cd19 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Autoscale/Resource/GroupConfiguration.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Autoscale/Resource/GroupConfiguration.php @@ -1,37 +1,43 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Autoscale\Resource; /** * This specifies the basic elements of the group. The Group Configuration contains: - * + * * - Group Name * - Group Cooldown (how long a group has to wait before you can scale again in seconds) * - Minimum and Maximum number of entities - * + * * @link https://github.com/rackerlabs/otter/blob/master/doc/getting_started.rst * @link http://docs.autoscale.apiary.io/ */ class GroupConfiguration extends AbstractResource { - public $name; public $cooldown; public $minEntities; public $maxEntities; public $metadata; - + protected static $json_name = 'groupConfiguration'; protected static $url_resource = 'config'; - + public $createKeys = array( 'name', 'cooldown', @@ -48,5 +54,4 @@ public function delete() { return $this->noDelete(); } - -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Autoscale/Resource/LaunchConfiguration.php b/rackspace/php-opencloud/lib/OpenCloud/Autoscale/Resource/LaunchConfiguration.php index 7b10493af..29951254c 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Autoscale/Resource/LaunchConfiguration.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Autoscale/Resource/LaunchConfiguration.php @@ -1,20 +1,28 @@ + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Autoscale\Resource; /** - * This configuration specifies what to do when we want to create a new server. + * This configuration specifies what to do when we want to create a new server. * What image to boot, on what flavor, and which load balancer to connect it to. - * + * * The Launch Configuration Contains: - * + * * - Launch Configuration Type (Only type currently supported is "launch_server") * - Arguments: * - Server @@ -24,24 +32,23 @@ * - Load Balancer * - loadBalancerId * - port - * + * * @link https://github.com/rackerlabs/otter/blob/master/doc/getting_started.rst * @link http://docs.autoscale.apiary.io/ */ class LaunchConfiguration extends AbstractResource { - public $type; public $args; - + protected static $json_name = 'launchConfiguration'; protected static $url_resource = 'launch'; - - public $createKeys = array( + + public $createKeys = array( 'type', 'args' ); - + /** * {@inheritDoc} */ @@ -49,7 +56,7 @@ public function create($params = array()) { return $this->noCreate(); } - + /** * {@inheritDoc} */ @@ -57,5 +64,4 @@ public function delete() { return $this->noDelete(); } - -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Autoscale/Resource/ScalingPolicy.php b/rackspace/php-opencloud/lib/OpenCloud/Autoscale/Resource/ScalingPolicy.php index 25b0257b0..5777345e7 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Autoscale/Resource/ScalingPolicy.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Autoscale/Resource/ScalingPolicy.php @@ -1,23 +1,27 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Autoscale\Resource; /** * Description of ScalingPolicy - * - * @link */ class ScalingPolicy extends AbstractResource { - public $id; public $links; public $name; @@ -25,37 +29,44 @@ class ScalingPolicy extends AbstractResource public $cooldown; public $type; public $metadata; - + protected static $json_name = 'policy'; protected static $json_collection_name = 'policies'; protected static $url_resource = 'policies'; - + public $createKeys = array( 'name', 'change', 'cooldown', 'type' ); - + public function getWebhookList() { return $this->getService()->resourceList('Webhook', null, $this); } - + public function getWebhook($id = null) { - $webhook = new Webhook(); - $webhook->setParent($this); - $webhook->setService($this->getService()); - if ($id) { - $webhook->populate($id); - } - return $webhook; + return $this->getService()->resource('Webhook', $id, $this); + } + + public function createWebhooks(array $webhooks) + { + $url = clone $this->getUrl(); + $url->addPath('webhooks'); + + $body = json_encode($webhooks); + $this->checkJsonError(); + + return $this->getService() + ->getClient() + ->post($url, self::getJsonHeader(), $body) + ->send(); } public function execute() { return $this->getClient()->post($this->url('execute'))->send(); } - -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Autoscale/Resource/Webhook.php b/rackspace/php-opencloud/lib/OpenCloud/Autoscale/Resource/Webhook.php index d3bc86e61..a71b34b86 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Autoscale/Resource/Webhook.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Autoscale/Resource/Webhook.php @@ -1,43 +1,48 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Autoscale\Resource; /** * Description of Webhook - * - * @link + * + * @link */ class Webhook extends AbstractResource { - public $id; public $name; public $metadata; public $links; - + protected static $json_name = 'webhook'; protected static $url_resource = 'webhooks'; - + public $createKeys = array( 'name', 'metadata' ); - + public function createJson() { $object = new \stdClass; $object->name = $this->name; $object->metadata = $this->metadata; - + return $object; } - -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Autoscale/Service.php b/rackspace/php-opencloud/lib/OpenCloud/Autoscale/Service.php index c2761ea2b..8ea09513e 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Autoscale/Service.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Autoscale/Service.php @@ -1,11 +1,18 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Autoscale; @@ -19,10 +26,10 @@ class Service extends CatalogService { const DEFAULT_TYPE = 'rax:autoscale'; const DEFAULT_NAME = 'autoscale'; - + /** * Autoscale resources. - * + * * @var array * @access private */ @@ -32,10 +39,10 @@ class Service extends CatalogService 'LaunchConfiguration', 'ScalingPolicy' ); - + /** * Convenience method for getting an autoscale group. - * + * * @param mixed $info * @return AbstractResource */ @@ -43,15 +50,14 @@ public function group($info = null) { return $this->resource('Group', $info); } - + /** * Convenience method for getting a list of autoscale groups. - * + * * @return OpenCloud\Common\Collection */ public function groupList() { return $this->resourceList('Group'); } - } diff --git a/rackspace/php-opencloud/lib/OpenCloud/CDN/Resource/Flavor.php b/rackspace/php-opencloud/lib/OpenCloud/CDN/Resource/Flavor.php new file mode 100644 index 000000000..de13eb9f2 --- /dev/null +++ b/rackspace/php-opencloud/lib/OpenCloud/CDN/Resource/Flavor.php @@ -0,0 +1,51 @@ +noUpdate(); + } + + protected function createJson() + { + $createJson = parent::createJson(); + return $createJson->{self::$json_name}; + } +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/CDN/Resource/Service.php b/rackspace/php-opencloud/lib/OpenCloud/CDN/Resource/Service.php new file mode 100644 index 000000000..5e3a2ff48 --- /dev/null +++ b/rackspace/php-opencloud/lib/OpenCloud/CDN/Resource/Service.php @@ -0,0 +1,115 @@ + 'flavorId', + 'http_host' => 'httpHost', + 'request_url' => 'requestUrl' + ); + + protected $createKeys = array( + 'name', + 'domains', + 'origins', + 'caching', + 'restrictions', + 'flavorId' + ); + + protected $updateKeys = array( + 'name', + 'domains', + 'origins', + 'caching', + 'restrictions', + 'flavorId' + ); + + public function purgeAssets($assetUrl = null) + { + $assetsUrl = $this->assetsUrl(); + if (null === $assetUrl) { + $assetsUrl->setQuery(array('all' => 'true')); + } else { + $assetsUrl->setQuery(array('url' => $assetUrl)); + } + + $request = $this->getClient()->delete($assetsUrl); + + // This is necessary because the response does not include a body + // and fails with a 406 Not Acceptable if the default + // 'Accept: application/json' header is used in the request. + $request->removeHeader('Accept'); + + return $request->send(); + } + + protected function assetsUrl() + { + $url = clone $this->getUrl(); + $url->addPath('assets'); + + return $url; + } + + protected function createJson() + { + $createJson = parent::createJson(); + return $createJson->{self::$json_name}; + } + + /** + * Update this resource + * + * @param array $params + * @return \Guzzle\Http\Message\Response + */ + public function update($params = array()) + { + $json = $this->generateJsonPatch($params); + + return $this->getClient() + ->patch($this->getUrl(), $this->getPatchHeaders(), $json) + ->send(); + } +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/CDN/Service.php b/rackspace/php-opencloud/lib/OpenCloud/CDN/Service.php new file mode 100644 index 000000000..57c880052 --- /dev/null +++ b/rackspace/php-opencloud/lib/OpenCloud/CDN/Service.php @@ -0,0 +1,188 @@ +resource('Service', $id); + } + + /** + * Creates a new Service and returns it. + * + * @see https://github.com/rackspace/php-opencloud/blob/master/docs/userguide/CDN/USERGUIDE.md#create-a-service + * @param array $params Service creation parameters. + * @return \OpenCloud\CDN\Resource\Service Object representing created service + */ + public function createService(array $params = array()) + { + $service = $this->service(); + $service->create($params); + return $service; + } + + /** + * Returns a Service object associated with this CDN service + * + * @param string $id ID of service to retrieve + * @return \OpenCloud\CDN\Resource\Service object + */ + public function getService($id) + { + return $this->service($id); + } + + /** + * Returns a list of services you created + * + * @param array $params + * @return \OpenCloud\Common\Collection\PaginatedIterator + */ + public function listServices(array $params = array()) + { + $params['limit'] = isset($params['limit']) && $params['limit'] <= self::MAX_LIMIT ?: self::MAX_LIMIT; + + $url = clone $this->getUrl(); + $url->addPath(ServiceResource::resourceName())->setQuery($params); + + return $this->resourceList('Service', $url); + } + + /** + * Returns a Flavor object associated with this CDN service + * + * @param string $id ID of flavor to retrieve + * @return \OpenCloud\CDN\Resource\Flavor object + */ + public function flavor($id = null) + { + return $this->resource('Flavor', $id); + } + + /** + * Creates a new Flavor and returns it. + * + * @see https://github.com/rackspace/php-opencloud/blob/master/docs/userguide/CDN/USERGUIDE.md#create-a-flavor + * @param array $params Flavor creation parameters. + * @return \OpenCloud\CDN\Resource\Flavor Object representing created flavor + */ + public function createFlavor(array $params = array()) + { + $flavor = $this->flavor(); + $flavor->create($params); + return $flavor; + } + + /** + * Returns a Flavor object associated with this CDN service + * + * @param string $id ID of flavor to retrieve + * @return \OpenCloud\CDN\Resource\Flavor object + */ + public function getFlavor($id) + { + return $this->flavor($id); + } + + /** + * Returns a list of flavors you created + * + * @param array $params + * @return \OpenCloud\Common\Collection\PaginatedIterator + */ + public function listFlavors(array $params = array()) + { + $url = clone $this->getUrl(); + $url->addPath(Flavor::resourceName())->setQuery($params); + + return $this->resourceList('Flavor', $url); + } + + /** + * Returns the home document for the CDN service + * + * @return \stdClass home document response + */ + public function getHomeDocument() + { + $url = clone $this->getUrl(); + + // This hack is necessary otherwise Guzzle will remove the trailing + // slash from the URL and the request will fail because the service + // expects the trailing slash :( + $url->setPath($url->getPath() . '/'); + + $response = $this->getClient()->get($url)->send(); + return Formatter::decode($response); + } + + /** + * Returns the ping (status) response for the CDN service + * + * @return Guzzle\Http\Message\Response + */ + public function getPing() + { + $url = clone $this->getUrl(); + $url->addPath('ping'); + + $request = $this->getClient()->get($url); + + // This is necessary because the response does not include a body + // and fails with a 406 Not Acceptable if the default + // 'Accept: application/json' header is used in the request. + $request->removeHeader('Accept'); + + return $request->send(); + } + + /** + * Return namespaces. + * + * @return array + */ + public function namespaces() + { + return array(); + } +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Collection/MonitoringIterator.php b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Collection/MonitoringIterator.php new file mode 100644 index 000000000..0f629bf39 --- /dev/null +++ b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Collection/MonitoringIterator.php @@ -0,0 +1,35 @@ +info->swap_free)) { + $parsed->info = array($parsed->info); + } + + return $parsed; + } +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Exception/AgentException.php b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Exception/AgentException.php index e7451433b..86bc04f74 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Exception/AgentException.php +++ b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Exception/AgentException.php @@ -1,15 +1,22 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\CloudMonitoring\Exception; class AgentException extends CloudMonitoringException { -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Exception/AlarmException.php b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Exception/AlarmException.php index e60979a45..5f01e222e 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Exception/AlarmException.php +++ b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Exception/AlarmException.php @@ -1,15 +1,22 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\CloudMonitoring\Exception; class AlarmException extends CloudMonitoringException { -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Exception/CheckException.php b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Exception/CheckException.php index f2fa08d08..737e81a10 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Exception/CheckException.php +++ b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Exception/CheckException.php @@ -1,15 +1,22 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\CloudMonitoring\Exception; class CheckException extends CloudMonitoringException { -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Exception/CloudMonitoringException.php b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Exception/CloudMonitoringException.php index 8fc4e71a8..fbc3564f7 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Exception/CloudMonitoringException.php +++ b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Exception/CloudMonitoringException.php @@ -1,11 +1,18 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\CloudMonitoring\Exception; @@ -14,4 +21,4 @@ class CloudMonitoringException extends BaseException { -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Exception/EntityException.php b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Exception/EntityException.php index 6d377645d..b10e92005 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Exception/EntityException.php +++ b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Exception/EntityException.php @@ -1,15 +1,22 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\CloudMonitoring\Exception; class EntityException extends CloudMonitoringException { -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Exception/MetricException.php b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Exception/MetricException.php index 6e6366dab..049a82b4a 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Exception/MetricException.php +++ b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Exception/MetricException.php @@ -1,15 +1,22 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\CloudMonitoring\Exception; class MetricException extends CloudMonitoringException { -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Exception/NotificationHistoryException.php b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Exception/NotificationHistoryException.php index 5ee43078f..fd6910537 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Exception/NotificationHistoryException.php +++ b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Exception/NotificationHistoryException.php @@ -1,15 +1,22 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\CloudMonitoring\Exception; class NotificationHistoryException extends CloudMonitoringException { -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Exception/NotificationPlanException.php b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Exception/NotificationPlanException.php index 6ecc9480e..2556f46ab 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Exception/NotificationPlanException.php +++ b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Exception/NotificationPlanException.php @@ -1,15 +1,22 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\CloudMonitoring\Exception; class NotificationPlanException extends CloudMonitoringException { -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Exception/ServiceException.php b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Exception/ServiceException.php index 8a6106e82..654632787 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Exception/ServiceException.php +++ b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Exception/ServiceException.php @@ -1,15 +1,22 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\CloudMonitoring\Exception; class ServiceException extends CloudMonitoringException { -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Exception/TestException.php b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Exception/TestException.php index 5ecfb25b9..e7988358f 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Exception/TestException.php +++ b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Exception/TestException.php @@ -1,15 +1,22 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\CloudMonitoring\Exception; class TestException extends CloudMonitoringException { -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Exception/ZoneException.php b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Exception/ZoneException.php index 3b085c7ba..0f8351a83 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Exception/ZoneException.php +++ b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Exception/ZoneException.php @@ -1,15 +1,22 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\CloudMonitoring\Exception; class ZoneException extends CloudMonitoringException { -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/AbstractResource.php b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/AbstractResource.php index d75611ab4..b4d0a878c 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/AbstractResource.php +++ b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/AbstractResource.php @@ -1,18 +1,25 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\CloudMonitoring\Resource; use OpenCloud\Common\Exceptions; -use OpenCloud\Common\PersistentObject; use OpenCloud\Common\Http\Message\Formatter; +use OpenCloud\Common\PersistentObject; abstract class AbstractResource extends PersistentObject { @@ -27,20 +34,20 @@ public function createJson() } $object = new \stdClass; - + foreach (static::$emptyObject as $key) { if ($property = $this->getProperty($key)) { $object->$key = $property; } } - + return $object; } protected function updateJson($params = array()) { - $object = (object) $params; - + $object = (object) $params; + foreach (static::$requiredKeys as $requiredKey) { if (!$this->getProperty($requiredKey)) { throw new Exceptions\UpdateError(sprintf( @@ -54,7 +61,7 @@ protected function updateJson($params = array()) /** * Retrieves a collection of resource objects. - * + * * @access public * @return void */ @@ -65,10 +72,10 @@ public function listAll() /** * Test the validity of certain parameters for the resource. - * + * * @access public * @param array $params (default: array()) - * @param bool $debug (default: false) + * @param bool $debug (default: false) * @return void */ public function testParams($params = array(), $debug = false) @@ -86,7 +93,7 @@ public function testParams($params = array(), $debug = false) /** * Test the validity of an existing resource. - * + * * @access public * @param bool $debug (default: false) * @return void @@ -102,5 +109,4 @@ public function test($debug = false) return Formatter::decode($response); } - -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/Agent.php b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/Agent.php index ca1a2f930..eead7c6f4 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/Agent.php +++ b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/Agent.php @@ -1,11 +1,18 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\CloudMonitoring\Resource; @@ -30,7 +37,7 @@ class Agent extends ReadOnlyResource * @var int UTC timestamp of last connection. */ private $last_connected; - + protected static $json_name = false; protected static $json_collection_name = 'values'; protected static $url_resource = 'agents'; @@ -40,15 +47,15 @@ class Agent extends ReadOnlyResource * @throws \OpenCloud\CloudMonitoring\Exception\AgentException */ public function getConnections() - { - if (!$this->getId()) { - throw new Exception\AgentException( - 'Please specify an "ID" value' - ); - } + { + if (!$this->getId()) { + throw new Exception\AgentException( + 'Please specify an "ID" value' + ); + } - return $this->getService()->resourceList('AgentConnection', $this->getUrl('connections')); - } + return $this->getService()->resourceList('AgentConnection', $this->getUrl('connections')); + } /** * @param $connectionId @@ -56,20 +63,30 @@ public function getConnections() * @throws \OpenCloud\CloudMonitoring\Exception\AgentException */ public function getConnection($connectionId) - { - if (!$this->getId()) { - throw new Exception\AgentException( - 'Please specify an "ID" value' - ); - } - - $url = clone $this->getUrl(); + { + if (!$this->getId()) { + throw new Exception\AgentException( + 'Please specify an "ID" value' + ); + } + + $url = clone $this->getUrl(); $url->addPath('connections')->addPath($connectionId); - $response = $this->getClient()->get($url)->send(); + $response = $this->getClient()->get($url)->send(); $body = Formatter::decode($response); - return $this->getService()->resource('AgentConnection', $body); - } - -} \ No newline at end of file + return $this->getService()->resource('AgentConnection', $body); + } + + /** + * Retrieves the agent host object responsible for monitoring certain + * metrics for an agent. + * + * @return AgentHost + */ + public function getAgentHost() + { + return $this->getService()->resource('AgentHost', null, $this); + } +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/AgentConnection.php b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/AgentConnection.php index 1e17fd36a..e4a36c107 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/AgentConnection.php +++ b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/AgentConnection.php @@ -1,11 +1,18 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\CloudMonitoring\Resource; @@ -26,5 +33,4 @@ class AgentConnection extends ReadOnlyResource protected static $json_name = false; protected static $json_collection_name = 'values'; protected static $url_resource = 'agents'; - -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/AgentHost.php b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/AgentHost.php index 442452139..b056c60c3 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/AgentHost.php +++ b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/AgentHost.php @@ -1,17 +1,24 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\CloudMonitoring\Resource; +use OpenCloud\CloudMonitoring\Collection\MonitoringIterator; use OpenCloud\CloudMonitoring\Exception; -use OpenCloud\Common\Collection\ResourceIterator; /** * Agent class. @@ -20,7 +27,7 @@ class AgentHost extends ReadOnlyResource { private $token; private $label; - + protected static $json_name = false; protected static $json_collection_name = 'info'; protected static $url_resource = 'host_info'; @@ -45,7 +52,9 @@ public function info($type) )); } - return $this->getService()->resourceList('AgentHostInfo', $this->getUrl($type), $this); + return MonitoringIterator::factory($this, array( + 'baseUrl' => $this->getUrl($type), + 'resourceClass' => 'AgentHostInfo' + )); } - -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/AgentHostInfo.php b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/AgentHostInfo.php index 94fe3b05b..7d3332f31 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/AgentHostInfo.php +++ b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/AgentHostInfo.php @@ -1,15 +1,22 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\CloudMonitoring\Resource; class AgentHostInfo extends AgentHost { -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/AgentTarget.php b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/AgentTarget.php index 903ca6794..b5b85025b 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/AgentTarget.php +++ b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/AgentTarget.php @@ -1,26 +1,31 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\CloudMonitoring\Resource; use OpenCloud\CloudMonitoring\Exception; -use OpenCloud\Common\Http\Message\Formatter; /** * Agent class. */ class AgentTarget extends ReadOnlyResource { - private $type = 'agent.filesystem'; - + protected static $json_name = 'targets'; protected static $json_collection_name = 'targets'; protected static $url_resource = 'targets'; @@ -38,6 +43,7 @@ class AgentTarget extends ReadOnlyResource public function getUrl($path = null, array $query = array()) { $path = "agent/check_types/{$this->type}/{$this->resourceName()}"; + return $this->getParent()->getUrl($path); } @@ -52,10 +58,9 @@ public function setType($type) $this->type = $type; } - + public function getType() { return $this->type; } - -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/AgentToken.php b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/AgentToken.php index 6b260ed62..43c7fc84e 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/AgentToken.php +++ b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/AgentToken.php @@ -1,10 +1,18 @@ + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\CloudMonitoring\Resource; @@ -17,16 +25,15 @@ class AgentToken extends AbstractResource private $id; private $token; private $label; - + protected static $json_name = false; protected static $json_collection_name = 'values'; protected static $url_resource = 'agent_tokens'; - + protected static $emptyObject = array( 'label', 'token' ); protected static $requiredKeys = array(); - -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/Alarm.php b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/Alarm.php index 6ed2082a9..f743ea0af 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/Alarm.php +++ b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/Alarm.php @@ -1,11 +1,18 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\CloudMonitoring\Resource; @@ -47,16 +54,16 @@ class Alarm extends AbstractResource * @var string A friendly label for an alarm. */ private $label; - + protected static $json_name = false; protected static $json_collection_name = 'values'; protected static $url_resource = 'alarms'; - + protected static $requiredKeys = array( 'check_id', 'notification_plan_id' ); - + protected static $emptyObject = array( 'check_id', 'notification_plan_id', @@ -65,7 +72,7 @@ class Alarm extends AbstractResource 'label', 'metadata' ); - + public function test($params = array(), $debug = false) { if (!isset($params['criteria'])) { @@ -73,14 +80,14 @@ public function test($params = array(), $debug = false) 'Please specify a "criteria" value' ); } - + if (!isset($params['check_data']) || !is_array($params['check_data'])) { throw new Exception\AlarmException( 'Please specify a "check_data" array' ); } - - $url = $this->getParent()->url('test-alarm'); + + $url = $this->getParent()->url('test-alarm'); $body = json_encode((object) $params); $response = $this->getService() @@ -111,6 +118,7 @@ public function getRecordedChecks() public function getNotificationHistoryForCheck($checkId) { $url = $this->getHistoryUrl()->addPath($checkId); + return $this->getService()->resourceList('NotificationHistory', $url, $this); } @@ -134,5 +142,4 @@ public function isDisabled() { return $this->getDisabled() === true; } - -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/Changelog.php b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/Changelog.php index 685b7dc44..8164ec66b 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/Changelog.php +++ b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/Changelog.php @@ -1,11 +1,18 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\CloudMonitoring\Resource; @@ -22,9 +29,8 @@ class Changelog extends ReadOnlyResource private $check_id; private $state; private $analyzed_by_monitoring_zone_id; - + protected static $json_name = 'changelogs/alarms'; protected static $json_collection_name = 'values'; protected static $url_resource = 'changelogs/alarms'; - -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/Check.php b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/Check.php index e8e882b16..c74a1c32f 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/Check.php +++ b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/Check.php @@ -1,11 +1,18 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\CloudMonitoring\Resource; @@ -99,7 +106,7 @@ class Check extends AbstractResource ); protected static $requiredKeys = array('type'); - + protected $associatedResources = array('CheckType' => 'CheckType'); protected $dataPointParams = array( @@ -116,7 +123,8 @@ public function testUrl($debug = false) if ($debug === true) { $params['debug'] = 'true'; } - return $this->getParent()->url('test-check', $params); + + return $this->getParent()->url('test-check', $params); } public function testExistingUrl($debug = false) @@ -202,7 +210,6 @@ public function fetchDataPoints($metricName, array $options = array()) $url->setQuery($parts); - return $this->getService()->resourceList('Metric', $url, $this); + return $this->getService()->resourceList('MetricDataPoint', $url, $this); } - -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/CheckType.php b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/CheckType.php index f4d1a5795..7a7316a11 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/CheckType.php +++ b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/CheckType.php @@ -1,11 +1,18 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\CloudMonitoring\Resource; @@ -41,5 +48,4 @@ class CheckType extends ReadOnlyResource protected static $json_name = false; protected static $url_resource = 'check_types'; protected static $json_collection_name = 'values'; - -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/Entity.php b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/Entity.php index 87e4928b5..0245a10b2 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/Entity.php +++ b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/Entity.php @@ -1,11 +1,18 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\CloudMonitoring\Resource; @@ -89,5 +96,4 @@ public function getAlarm($id = null) { return $this->getService()->resource('Alarm', $id, $this); } - -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/Metric.php b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/Metric.php index bf6b7a4fb..0917b5da9 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/Metric.php +++ b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/Metric.php @@ -1,11 +1,18 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\CloudMonitoring\Resource; @@ -18,4 +25,4 @@ class Metric extends ReadOnlyResource protected static $json_name = 'metrics'; protected static $json_collection_name = 'values'; protected static $url_resource = 'metrics'; -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/MetricDataPoint.php b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/MetricDataPoint.php new file mode 100644 index 000000000..d0fe609df --- /dev/null +++ b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/MetricDataPoint.php @@ -0,0 +1,22 @@ + - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\CloudMonitoring\Resource; @@ -36,11 +43,11 @@ class Notification extends AbstractResource * @var array A hash of notification specific details based on the notification type. */ private $details; - + protected static $json_name = false; protected static $json_collection_name = 'values'; protected static $url_resource = 'notifications'; - + protected static $emptyObject = array( 'label', 'type', @@ -51,16 +58,16 @@ class Notification extends AbstractResource 'type', 'details' ); - + protected $associatedResources = array( 'NotificationType' => 'NotificationType' ); - + public function testUrl($debug = false) { return $this->getService()->getUrl('test-notification'); } - + public function test($debug = false) { $response = $this->getService() @@ -70,5 +77,4 @@ public function test($debug = false) return Formatter::decode($response); } - -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/NotificationHistory.php b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/NotificationHistory.php index a15979b30..7d0ef3510 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/NotificationHistory.php +++ b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/NotificationHistory.php @@ -1,11 +1,18 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\CloudMonitoring\Resource; @@ -25,7 +32,7 @@ class NotificationHistory extends ReadOnlyResource private $state; private $notification_results; private $previous_state; - + protected static $json_name = false; protected static $json_collection_name = 'values'; protected static $url_resource = 'notification_history'; @@ -33,9 +40,10 @@ class NotificationHistory extends ReadOnlyResource public function listChecks() { $response = $this->getClient()->get($this->url())->send(); + return Formatter::decode($response); } - + public function listHistory($checkId) { return $this->getService()->collection(get_class(), $this->url($checkId)); @@ -45,12 +53,11 @@ public function getSingleHistoryItem($checkId, $historyId) { $url = $this->url($checkId . '/' . $historyId); $response = $this->getClient()->get($url)->send(); - + if (null !== ($decoded = Formatter::decode($response))) { $this->populate($decoded); } return false; } - -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/NotificationPlan.php b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/NotificationPlan.php index d081a66d4..849495abe 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/NotificationPlan.php +++ b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/NotificationPlan.php @@ -1,11 +1,18 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\CloudMonitoring\Resource; @@ -36,7 +43,7 @@ class NotificationPlan extends AbstractResource * @var array The notification list to send to when the state is WARNING. */ private $warning_state; - + protected static $json_name = false; protected static $json_collection_name = 'values'; protected static $url_resource = 'notification_plans'; @@ -44,12 +51,11 @@ class NotificationPlan extends AbstractResource protected static $requiredKeys = array( 'label' ); - + protected static $emptyObject = array( 'label', 'critical_state', 'ok_state', 'warning_state' ); - -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/NotificationType.php b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/NotificationType.php index f7c51d9e0..addd9d1f1 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/NotificationType.php +++ b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/NotificationType.php @@ -1,11 +1,18 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\CloudMonitoring\Resource; @@ -18,9 +25,8 @@ class NotificationType extends ReadOnlyResource private $id; private $address; private $fields; - + protected static $json_name = false; protected static $json_collection_name = 'values'; protected static $url_resource = 'notification_types'; - -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/ReadOnlyResource.php b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/ReadOnlyResource.php index 38dfcb8ea..4fd3681f3 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/ReadOnlyResource.php +++ b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/ReadOnlyResource.php @@ -1,36 +1,41 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\CloudMonitoring\Resource; /** * ReadonlyResource class. - * + * * @extends AbstractResource */ -class ReadonlyResource extends AbstractResource +class ReadOnlyResource extends AbstractResource { - - public function create($params = array()) - { - return $this->noCreate(); + public function create($params = array()) + { + return $this->noCreate(); } - public function update($params = array()) - { - return $this->noUpdate(); + public function update($params = array()) + { + return $this->noUpdate(); } - public function delete($params = array()) - { - return $this->noDelete(); + public function delete($params = array()) + { + return $this->noDelete(); } - -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/View.php b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/View.php index e3bafea19..f64bd0a1e 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/View.php +++ b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/View.php @@ -1,11 +1,18 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\CloudMonitoring\Resource; @@ -20,15 +27,15 @@ class View extends ReadOnlyResource private $alarms; private $checks; private $latest_alarm_states; - + protected static $json_name = false; protected static $json_collection_name = 'values'; protected static $url_resource = 'views/overview'; - + protected $associatedResources = array( 'entity' => 'Entity' ); - + protected $associatedCollections = array( 'alarms' => 'Alarm', 'checks' => 'Check' @@ -43,5 +50,4 @@ public function getCheck($info = null) { return $this->getService()->resource('Check', $info); } - -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/Zone.php b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/Zone.php index e7c2975f6..acf5ec2e8 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/Zone.php +++ b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Resource/Zone.php @@ -1,17 +1,25 @@ + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\CloudMonitoring\Resource; +use Guzzle\Http\Exception\ClientErrorResponseException; use OpenCloud\CloudMonitoring\Exception; use OpenCloud\Common\Http\Message\Formatter; -use Guzzle\Http\Exception\ClientErrorResponseException; /** * Zone class. @@ -25,7 +33,7 @@ class Zone extends ReadOnlyResource private $country_code; /** @var string */ - private $label; + private $label; /** @var array List of source IPs */ private $source_ips; @@ -39,9 +47,9 @@ public function traceroute(array $options) if (!$this->getId()) { throw new Exception\ZoneException( 'Please specify a zone ID' - ); + ); } - + if (!isset($options['target']) || !isset($options['target_resolver'])) { throw new Exception\ZoneException( 'Please specify a "target" and "target_resolver" value' @@ -49,7 +57,7 @@ public function traceroute(array $options) } $params = (object) array( - 'target' => $options['target'], + 'target' => $options['target'], 'target_resolver' => $options['target_resolver'] ); try { @@ -61,10 +69,8 @@ public function traceroute(array $options) $body = Formatter::decode($response); return (isset($body->result)) ? $body->result : false; - } catch (ClientErrorResponseException $e) { return false; } } - -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Service.php b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Service.php index 660cf5e70..8733dd0a7 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Service.php +++ b/rackspace/php-opencloud/lib/OpenCloud/CloudMonitoring/Service.php @@ -1,10 +1,18 @@ + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\CloudMonitoring; @@ -64,9 +72,9 @@ public function getAgents() return $this->resourceList('Agent'); } - public function getAgentHost($id = null) + public function getAgentHost($agentId = null) { - return $this->resource('AgentHost', $id); + return $this->resource('AgentHost', null, $this->getAgent($agentId)); } public function getAgentTargets() @@ -277,5 +285,4 @@ public function getViews() { return $this->resourceList('View'); } - -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Common/ArrayAccess.php b/rackspace/php-opencloud/lib/OpenCloud/Common/ArrayAccess.php new file mode 100644 index 000000000..698c7ec0f --- /dev/null +++ b/rackspace/php-opencloud/lib/OpenCloud/Common/ArrayAccess.php @@ -0,0 +1,60 @@ +elements = (array) $data; + } + + /** + * Sets a value to a particular offset. + * + * @param mixed $offset + * @param mixed $value + */ + public function offsetSet($offset, $value) + { + if ($offset === null) { + $this->elements[] = $value; + } else { + $this->elements[$offset] = $value; + } + } + + /** + * Checks to see whether a particular offset key exists. + * + * @param mixed $offset + * @return bool + */ + public function offsetExists($offset) + { + return array_key_exists($offset, $this->elements); + } + + /** + * Unset a particular key. + * + * @param mixed $offset + */ + public function offsetUnset($offset) + { + unset($this->elements[$offset]); + } + + /** + * Get the value for a particular offset key. + * + * @param mixed $offset + * @return mixed|null + */ + public function offsetGet($offset) + { + return $this->offsetExists($offset) ? $this->elements[$offset] : null; + } +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Common/Base.php b/rackspace/php-opencloud/lib/OpenCloud/Common/Base.php index 699f400f1..82bf0061b 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Common/Base.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Common/Base.php @@ -1,21 +1,27 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Common; -use OpenCloud\Common\Collection\PaginatedIterator; -use OpenCloud\Common\Exceptions\JsonError; -use OpenCloud\Common\Exceptions\UrlError; use OpenCloud\Common\Collection\ResourceIterator; use OpenCloud\Common\Constants\Header as HeaderConst; use OpenCloud\Common\Constants\Mime as MimeConst; +use OpenCloud\Common\Exceptions\JsonError; +use Psr\Log\LoggerInterface; /** * The root class for all other objects used or defined by this SDK. @@ -26,19 +32,29 @@ */ abstract class Base { + const PATCH_CONTENT_TYPE = MimeConst::JSON_PATCH; + /** - * @var array Holds all the properties added by overloading. + * Holds all the properties added by overloading. + * + * @var array */ private $properties = array(); /** - * Debug status. + * The logger instance * - * @var LoggerInterface - * @access private + * @var LoggerInterface */ private $logger; + /** + * The aliases configure for the properties of the instance. + * + * @var array + */ + protected $aliases = array(); + /** * @return static */ @@ -70,14 +86,14 @@ public function __call($method, $args) if ($this->propertyExists($property) && $prefix == 'set') { return $this->setProperty($property, $args[0]); } - + throw new Exceptions\RuntimeException(sprintf( - 'No method %s::%s()', - get_class($this), - $method - )); + 'No method %s::%s()', + get_class($this), + $method + )); } - + /** * We can set a property under three conditions: * @@ -94,11 +110,8 @@ protected function setProperty($property, $value) $setter = 'set' . $this->toCamel($property); if (method_exists($this, $setter)) { - return call_user_func(array($this, $setter), $value); - - } elseif (false !== ($propertyVal = $this->propertyExists($property))) { - + } elseif (false !== ($propertyVal = $this->propertyExists($property))) { // Are we setting a public or private property? if ($this->isAccessible($propertyVal)) { $this->$propertyVal = $value; @@ -107,9 +120,7 @@ protected function setProperty($property, $value) } return $this; - } else { - $this->getLogger()->warning( 'Attempted to set {property} with value {value}, but the' . ' property has not been defined. Please define first.', @@ -150,12 +161,13 @@ protected function propertyExists($property, $allowRetry = true) * @param bool $capitalise Optional flag which allows for word capitalization. * @return mixed */ - function toCamel($string, $capitalise = true) + public function toCamel($string, $capitalise = true) { if ($capitalise) { $string = ucfirst($string); } - return preg_replace_callback('/_([a-z])/', function($char) { + + return preg_replace_callback('/_([a-z])/', function ($char) { return strtoupper($char[1]); }, $string); } @@ -166,10 +178,11 @@ function toCamel($string, $capitalise = true) * @param $string * @return mixed */ - function toUnderscores($string) + public function toUnderscores($string) { $string = lcfirst($string); - return preg_replace_callback('/([A-Z])/', function($char) { + + return preg_replace_callback('/([A-Z])/', function ($char) { return "_" . strtolower($char[1]); }, $string); } @@ -184,7 +197,7 @@ private function isAccessible($property) { return array_key_exists($property, get_object_vars($this)); } - + /** * Checks the attribute $property and only permits it if the prefix is * in the specified $prefixes array @@ -200,9 +213,10 @@ private function checkAttributePrefix($property) return false; } $prefix = strstr($property, ':', true); + return in_array($prefix, $this->getService()->namespaces()); } - + /** * Grab value out of the data array. * @@ -220,35 +234,46 @@ protected function getProperty($property) } elseif (false !== ($propertyVal = $this->propertyExists($property)) && $this->isAccessible($propertyVal)) { return $this->$propertyVal; } - + return null; } - + /** * Sets the logger. * - * @param Log\LoggerInterface $logger + * @param LoggerInterface $logger + * * @return $this */ - public function setLogger(Log\LoggerInterface $logger) + public function setLogger(LoggerInterface $logger = null) { $this->logger = $logger; + return $this; } /** * Returns the Logger object. - * - * @return \OpenCloud\Common\Log\AbstractLogger + * + * @return LoggerInterface */ public function getLogger() { if (null === $this->logger) { $this->setLogger(new Log\Logger); } + return $this->logger; } + /** + * @return bool + */ + public function hasLogger() + { + return (null !== $this->logger); + } + /** * @deprecated */ @@ -259,83 +284,61 @@ public function url($path = null, array $query = array()) /** * Populates the current object based on an unknown data type. - * + * * @param mixed $info - * @param bool - * @throws Exceptions\InvalidArgumentError - */ - /** - * @param mixed $info The data structure that is populating the object. - * @param bool $setObjects If set to TRUE, then this method will try to populate associated resources as objects - * rather than anonymous data types. So, a Server being populated might stock a Network - * object instead of a stdClass object. + * @param bool * @throws Exceptions\InvalidArgumentError */ public function populate($info, $setObjects = true) { if (is_string($info) || is_integer($info)) { - $this->setProperty($this->primaryKeyField(), $info); $this->refresh($info); - } elseif (is_object($info) || is_array($info)) { - foreach ($info as $key => $value) { - if ($key == 'metadata' || $key == 'meta') { - // Try retrieving existing value if (null === ($metadata = $this->getProperty($key))) { // If none exists, create new object $metadata = new Metadata; } - + // Set values for metadata $metadata->setArray($value); - + // Set object property $this->setProperty($key, $metadata); - } elseif (!empty($this->associatedResources[$key]) && $setObjects === true) { - // Associated resource try { - $resource = $this->getService()->resource($this->associatedResources[$key], $value); $resource->setParent($this); $this->setProperty($key, $resource); - - } catch (Exception\ServiceException $e) {} - + } catch (Exception\ServiceException $e) { + } } elseif (!empty($this->associatedCollections[$key]) && $setObjects === true) { - // Associated collection try { - $className = $this->associatedCollections[$key]; $options = $this->makeResourceIteratorOptions($className); $iterator = ResourceIterator::factory($this, $options, $value); $this->setProperty($key, $iterator); - - } catch (Exception\ServiceException $e) {} - + } catch (Exception\ServiceException $e) { + } } elseif (!empty($this->aliases[$key])) { - // Sometimes we might want to preserve camelCase // or covert `rax-bandwidth:bandwidth` to `raxBandwidth` $this->setProperty($this->aliases[$key], $value); - } else { // Normal key/value pair $this->setProperty($key, $value); } - } } elseif (null !== $info) { throw new Exceptions\InvalidArgumentError(sprintf( - Lang::translate('Argument for [%s] must be string or object'), + Lang::translate('Argument for [%s] must be string or object'), get_class() )); } @@ -371,7 +374,7 @@ public static function checkJsonError() $jsonError = 'Unexpected JSON error'; break; } - + if (isset($jsonError)) { throw new JsonError(Lang::translate($jsonError)); } @@ -418,6 +421,7 @@ public function makeResourceIteratorOptions($resource) public function stripNamespace($namespace) { $array = explode('\\', $namespace); + return end($array); } @@ -426,4 +430,8 @@ protected static function getJsonHeader() return array(HeaderConst::CONTENT_TYPE => MimeConst::JSON); } + protected static function getPatchHeaders() + { + return array(HeaderConst::CONTENT_TYPE => static::PATCH_CONTENT_TYPE); + } } diff --git a/rackspace/php-opencloud/lib/OpenCloud/Common/Collection.php b/rackspace/php-opencloud/lib/OpenCloud/Common/Collection.php index 0aa5552f4..f15e83c47 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Common/Collection.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Common/Collection.php @@ -1,21 +1,30 @@ + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Common; +use OpenCloud\Common\Log\Logger; + /** * @deprecated * @codeCoverageIgnore */ class Collection extends Base { - private $service; private $itemClass; private $itemList = array(); @@ -36,14 +45,14 @@ class Collection extends Base * * It assumes that the factory method can take an array of values, and * it passes that to the method. * - * @param Service $service - the service associated with the collection - * @param string $itemclass - the Class of each item in the collection - * (assumed to be the name of the factory method) - * @param array $arr - the input array + * @param Service $service - the service associated with the collection + * @param string $itemclass - the Class of each item in the collection + * (assumed to be the name of the factory method) + * @param array $arr - the input array */ public function __construct($service, $class, array $array = array()) { - $service->getLogger()->deprecated(__METHOD__, 'OpenCloud\Common\Collection\CollectionBuilder'); + $service->getLogger()->warning(Logger::deprecated(__METHOD__, 'OpenCloud\Common\Collection\CollectionBuilder')); $this->setService($service); @@ -68,6 +77,7 @@ public function __construct($service, $class, array $array = array()) private function setItemList(array $array) { $this->itemList = $array; + return $this; } @@ -89,6 +99,7 @@ public function getItemList() public function setService($service) { $this->service = $service; + return $this; } @@ -108,6 +119,7 @@ public function getService() private function setItemClass($itemClass) { $this->itemClass = $itemClass; + return $this; } @@ -125,6 +137,7 @@ private function getItemClass() private function setSortKey($sortKey) { $this->sortKey = $sortKey; + return $this; } @@ -142,6 +155,7 @@ private function getSortKey() private function setNextPageClass($nextPageClass) { $this->nextPageClass = $nextPageClass; + return $this; } @@ -164,14 +178,15 @@ private function getNextPageClass() * and the `$url` should be the URL of the next page of results * * @param callable $callback the name of the function (or array of - * object, function name) - * @param string $url the URL of the next page of results + * object, function name) + * @param string $url the URL of the next page of results * @return void */ public function setNextPageCallback($callback, $url) { $this->nextPageCallback = $callback; $this->nextPageUrl = $url; + return $this; } @@ -238,6 +253,7 @@ public function reset() public function first() { $this->reset(); + return $this->next(); } @@ -274,7 +290,7 @@ public function next() return false; } - $data = $this->getItem($this->pointer++); + $data = $this->getItem($this->pointer++); $class = $this->getItemClass(); // Are there specific methods in the parent/service that can be used to @@ -327,7 +343,7 @@ public function sort($keyname = 'id') * Example: * * $services = $connection->ServiceList(); - * $services->Select(function($item){ return $item->region=='ORD';}); + * $services->Select(function ($item) { return $item->region=='ORD';}); * // now the $services Collection only has items from the ORD region * * @@ -338,9 +354,9 @@ public function sort($keyname = 'id') * * @api * @param callable $testfunc a callback function that is passed each item - * in turn. Note that `Select()` performs an explicit test for - * `FALSE`, so functions like `strpos()` need to be cast into a - * boolean value (and not just return the integer). + * in turn. Note that `Select()` performs an explicit test for + * `FALSE`, so functions like `strpos()` need to be cast into a + * boolean value (and not just return the integer). * @returns void * @throws DomainError if callback doesn't return a boolean value */ @@ -368,7 +384,7 @@ public function select($testfunc) * * $coll = $obj->Collection(); * do { - * while($item = $coll->Next()) { + * while ($item = $coll->Next()) { * // do something with the item * } * } while ($coll = $coll->NextPage()); @@ -404,5 +420,4 @@ private function sortCompare($a, $b) return 1; } } - -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Common/Collection/ArrayCollection.php b/rackspace/php-opencloud/lib/OpenCloud/Common/Collection/ArrayCollection.php index 9d53b54bb..01e524388 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Common/Collection/ArrayCollection.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Common/Collection/ArrayCollection.php @@ -1,24 +1,31 @@ + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Common\Collection; use Countable; -use ArrayAccess; +use OpenCloud\Common\ArrayAccess; /** * A generic, abstract collection class that allows collections to exhibit array functionality. * * @package OpenCloud\Common\Collection - * @since 1.8.0 */ -abstract class ArrayCollection implements ArrayAccess, Countable +abstract class ArrayCollection extends ArrayAccess implements Countable { /** * @var array The elements being held by this iterator. @@ -48,18 +55,8 @@ public function count() public function setElements(array $data = array()) { $this->elements = $data; - return $this; - } - /** - * Sets a value to a particular offset. - * - * @param mixed $offset - * @param mixed $value - */ - public function offsetSet($offset, $value) - { - $this->elements[$offset] = $value; + return $this; } /** @@ -72,17 +69,6 @@ public function append($value) $this->elements[] = $value; } - /** - * Checks to see whether a particular offset key exists. - * - * @param mixed $offset - * @return bool - */ - public function offsetExists($offset) - { - return isset($this->elements[$offset]); - } - /** * Checks to see whether a particular value exists. * @@ -93,26 +79,4 @@ public function valueExists($value) { return array_search($value, $this->elements) !== false; } - - /** - * Unset a particular key. - * - * @param mixed $offset - */ - public function offsetUnset($offset) - { - unset($this->elements[$offset]); - } - - /** - * Get the value for a particular offset key. - * - * @param mixed $offset - * @return mixed|null - */ - public function offsetGet($offset) - { - return $this->offsetExists($offset) ? $this->elements[$offset] : null; - } - -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Common/Collection/PaginatedIterator.php b/rackspace/php-opencloud/lib/OpenCloud/Common/Collection/PaginatedIterator.php index 7990580ae..ef827ac80 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Common/Collection/PaginatedIterator.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Common/Collection/PaginatedIterator.php @@ -1,17 +1,25 @@ + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Common\Collection; -use Iterator; -use Guzzle\Http\Url; use Guzzle\Http\Exception\ClientErrorResponseException; +use Guzzle\Http\Url; +use Iterator; use OpenCloud\Common\Http\Message\Formatter; /** @@ -25,7 +33,7 @@ class PaginatedIterator extends ResourceIterator implements Iterator { const MARKER = 'marker'; - const LIMIT = 'limit'; + const LIMIT = 'limit'; /** * @var string Used for requests which append elements. @@ -39,24 +47,24 @@ class PaginatedIterator extends ResourceIterator implements Iterator protected $defaults = array( // Collection limits - 'limit.total' => 10000, - 'limit.page' => 100, + 'limit.total' => 10000, + 'limit.page' => 100, // The "links" element key in response - 'key.links' => 'links', + 'key.links' => 'links', // JSON structure - 'key.collection' => null, + 'key.collection' => null, 'key.collectionElement' => null, // The property used as the marker - 'key.marker' => 'name', + 'key.marker' => 'name', // Options for "next page" request - 'request.method' => 'GET', - 'request.headers' => array(), - 'request.body' => null, - 'request.curlOptions' => array() + 'request.method' => 'GET', + 'request.headers' => array(), + 'request.body' => null, + 'request.curlOptions' => array() ); protected $required = array('resourceClass', 'baseUrl'); @@ -64,11 +72,10 @@ class PaginatedIterator extends ResourceIterator implements Iterator /** * Basic factory method to easily instantiate a new ResourceIterator. * - * @param $parent The parent object - * @param Url $url The base URL - * @param array $params Options for this iterator + * @param $parent The parent object + * @param array $options Iterator options + * @param array $data Optional data to set initially * @return static - * @throws \OpenCloud\Common\Exceptions\InvalidArgumentError */ public static function factory($parent, array $options = array(), array $data = null) { @@ -95,6 +102,7 @@ public static function factory($parent, array $options = array(), array $data = public function setBaseUrl(Url $url) { $this->baseUrl = $url; + return $this; } @@ -137,7 +145,11 @@ public function updateMarkerToCurrent() } $element = $this->elements[$this->position]; + $this->setMarkerFromElement($element); + } + protected function setMarkerFromElement($element) + { $key = $this->getOption('key.marker'); if (isset($element->$key)) { @@ -157,7 +169,8 @@ public function rewind() public function valid() { - if ($this->getOption('limit.total') !== false && $this->position >= $this->getOption('limit.total')) { + $totalLimit = $this->getOption('limit.total'); + if ($totalLimit !== false && $this->position >= $totalLimit) { return false; } elseif (isset($this->elements[$this->position])) { return true; @@ -172,7 +185,10 @@ public function valid() protected function shouldAppend() { - return $this->currentMarker && $this->position % $this->getOption('limit.page') == 0; + return $this->currentMarker && ( + $this->nextUrl || + $this->position % $this->getOption('limit.page') == 0 + ); } /** @@ -184,6 +200,7 @@ protected function shouldAppend() public function appendElements(array $elements) { $this->elements = array_merge($this->elements, $elements); + return $this; } @@ -252,7 +269,6 @@ public function extractNextLink($body) public function constructNextUrl() { if (!$url = $this->nextUrl) { - $url = clone $this->getOption('baseUrl'); $query = $url->getQuery(); @@ -310,5 +326,4 @@ public function populateAll() $this->next(); } } - } diff --git a/rackspace/php-opencloud/lib/OpenCloud/Common/Collection/ResourceIterator.php b/rackspace/php-opencloud/lib/OpenCloud/Common/Collection/ResourceIterator.php index 4e2a973ee..c0054b8d1 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Common/Collection/ResourceIterator.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Common/Collection/ResourceIterator.php @@ -1,17 +1,25 @@ + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Common\Collection; use Iterator; -use OpenCloud\Common\Log\Logger; use OpenCloud\Common\Exceptions\InvalidArgumentError; +use OpenCloud\Common\Log\Logger; class ResourceIterator extends ArrayCollection implements Iterator { @@ -71,6 +79,7 @@ protected function parseOptions(array $options) public function setResourceParent($parent) { $this->resourceParent = $parent; + return $this; } @@ -81,9 +90,18 @@ public function setResourceParent($parent) public function setOptions(array $options) { $this->options = $options; + return $this; } + /** + * @return array Options for the resource iterator. + */ + public function getOptions() + { + return $this->options; + } + /** * Set a particular option. * @@ -94,6 +112,7 @@ public function setOptions(array $options) public function setOption($key, $value) { $this->options[$key] = $value; + return $this; } @@ -122,6 +141,7 @@ public function valid() public function next() { $this->position++; + return $this->current(); } @@ -203,7 +223,8 @@ public function getElement($offset) */ public function first() { - Logger::newInstance()->deprecated(__METHOD__, 'getElement'); + Logger::newInstance()->warning(Logger::deprecated(__METHOD__, 'getElement')); + return $this->getElement(0); } @@ -232,5 +253,4 @@ public function search($callback) return $return; } - -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Common/Constants/Datetime.php b/rackspace/php-opencloud/lib/OpenCloud/Common/Constants/Datetime.php index 750d1d533..9a5e76ffb 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Common/Constants/Datetime.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Common/Constants/Datetime.php @@ -1,35 +1,38 @@ - * @author Glen Campbell + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Common\Constants; - -class Datetime +class Datetime { /** * Values in s. */ const SECOND = 1; const MINUTE = 60; - const HOUR = 3600; - const DAY = 86400; + const HOUR = 3600; + const DAY = 86400; /** * Values in ms. */ const MILLISECOND = 1; - const SECOND_M = 1000; - const MINUTE_M = 60000; - const HOUR_M = 3600000; - const DAY_M = 86400000; - - - -} \ No newline at end of file + const SECOND_M = 1000; + const MINUTE_M = 60000; + const HOUR_M = 3600000; + const DAY_M = 86400000; +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Common/Constants/Header.php b/rackspace/php-opencloud/lib/OpenCloud/Common/Constants/Header.php index e09961cea..5de04bc2a 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Common/Constants/Header.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Common/Constants/Header.php @@ -1,10 +1,18 @@ + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Common\Constants; @@ -15,51 +23,52 @@ * @link http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html * @package OpenCloud\Common\Constants */ -class Header +class Header { - const ACCEPT = 'Accept'; - const ACCEPT_CHARSET = 'Accept-Charset'; - const ACCEPT_ENCODING = 'Accept-Encoding'; - const ACCEPT_LANGUAGE = 'Accept-Language'; - const ACCEPT_RANGES = 'Accept-Ranges'; - const AGE = 'Age'; - const ALLOW = 'Allow'; - const AUTHORIZATION = 'Authorization'; - const CACHE_CONTROL = 'Cache-Control'; - const CONNECTION = 'Connection'; - const CONTENT_ENCODING = 'Content-Encoding'; - const CONTENT_LANGUAGE = 'Content-Language'; - const CONTENT_LENGTH = 'Content-Length'; - const CONTENT_LOCATION = 'Content-Location'; - const CONTENT_MD5 = 'Content-MD5'; - const CONTENT_RANGE = 'Content-Range'; - const CONTENT_TYPE = 'Content-Type'; - const DATE = 'Date'; - const ETAG = 'ETag'; - const EXPECT = 'Expect'; - const EXPIRES = 'Expires'; - const FROM = 'From'; - const HOST = 'Host'; - const IF_MATCH = 'If-Match'; - const IF_MODIFIED_SINCE = 'If-Modified-Since'; - const IF_NONE_MATCH = 'If-None-Match'; - const IF_RANGE = 'If-Range'; - const IF_UNMODIFIED_SINCE = 'If-Unmodified-Since'; - const LAST_MODIFIED = 'Last-Modified'; - const LOCATION = 'Location'; - const MAX_FORWARDS = 'Max-Forwards'; - const PRAGMA = 'Pragma'; - const PROXY_AUTHENTICATION = 'Proxy-Authenticate'; - const PROXY_AUTHORIZATION = 'Proxy-Authorization'; - const RANGE = 'Range'; - const REFERER = 'Referer'; - const RETRY_AFTER = 'Retry-After'; - const SERVER = 'Server'; - const TE = 'TE'; - const TRAILER = 'Trailer'; - const TRANSFER_ENCODING = 'Transfer-Encoding'; - const UPGRADE = 'Upgrade'; - const USER_AGENT = 'User-Agent'; - const VARY = 'Vary'; - const VIA = 'Via'; -} \ No newline at end of file + const ACCEPT = 'Accept'; + const ACCEPT_CHARSET = 'Accept-Charset'; + const ACCEPT_ENCODING = 'Accept-Encoding'; + const ACCEPT_LANGUAGE = 'Accept-Language'; + const ACCEPT_RANGES = 'Accept-Ranges'; + const AGE = 'Age'; + const ALLOW = 'Allow'; + const AUTHORIZATION = 'Authorization'; + const CACHE_CONTROL = 'Cache-Control'; + const CONNECTION = 'Connection'; + const CONTENT_ENCODING = 'Content-Encoding'; + const CONTENT_LANGUAGE = 'Content-Language'; + const CONTENT_LENGTH = 'Content-Length'; + const CONTENT_LOCATION = 'Content-Location'; + const CONTENT_MD5 = 'Content-MD5'; + const CONTENT_RANGE = 'Content-Range'; + const CONTENT_TYPE = 'Content-Type'; + const DATE = 'Date'; + const ETAG = 'ETag'; + const EXPECT = 'Expect'; + const EXPIRES = 'Expires'; + const FROM = 'From'; + const HOST = 'Host'; + const IF_MATCH = 'If-Match'; + const IF_MODIFIED_SINCE = 'If-Modified-Since'; + const IF_NONE_MATCH = 'If-None-Match'; + const IF_RANGE = 'If-Range'; + const IF_UNMODIFIED_SINCE = 'If-Unmodified-Since'; + const LAST_MODIFIED = 'Last-Modified'; + const LOCATION = 'Location'; + const MAX_FORWARDS = 'Max-Forwards'; + const PRAGMA = 'Pragma'; + const PROXY_AUTHENTICATION = 'Proxy-Authenticate'; + const PROXY_AUTHORIZATION = 'Proxy-Authorization'; + const RANGE = 'Range'; + const REFERER = 'Referer'; + const RETRY_AFTER = 'Retry-After'; + const SERVER = 'Server'; + const TE = 'TE'; + const TRAILER = 'Trailer'; + const TRANSFER_ENCODING = 'Transfer-Encoding'; + const UPGRADE = 'Upgrade'; + const USER_AGENT = 'User-Agent'; + const VARY = 'Vary'; + const VIA = 'Via'; + const X_OBJECT_MANIFEST = 'X-Object-Manifest'; +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Common/Constants/Mime.php b/rackspace/php-opencloud/lib/OpenCloud/Common/Constants/Mime.php index c2f717826..4e2942ebb 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Common/Constants/Mime.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Common/Constants/Mime.php @@ -1,17 +1,25 @@ + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Common\Constants; - -class Mime +class Mime { const JSON = 'application/json'; const TEXT = 'text/plain'; -} \ No newline at end of file + const JSON_PATCH = 'application/json-patch+json'; +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Common/Constants/Service.php b/rackspace/php-opencloud/lib/OpenCloud/Common/Constants/Service.php index 7420f0c4c..636e535b5 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Common/Constants/Service.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Common/Constants/Service.php @@ -1,19 +1,24 @@ - * @author Glen Campbell + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Common\Constants; - -class Service +class Service { - const INTERNAL_URL = 'publicUrl'; - const PUBLIC_URL = 'internalUrl'; - -} \ No newline at end of file + const INTERNAL_URL = 'internalUrl'; + const PUBLIC_URL = 'publicUrl'; +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Common/Constants/Size.php b/rackspace/php-opencloud/lib/OpenCloud/Common/Constants/Size.php index 195954e80..5d3073277 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Common/Constants/Size.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Common/Constants/Size.php @@ -1,20 +1,26 @@ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Common\Constants; class Size { - - const KB = 1024; - const MB = 1048576; - const GB = 1073741824; - const TB = 1099511627776; - -} \ No newline at end of file + const KB = 1024; + const MB = 1048576; + const GB = 1073741824; + const TB = 1099511627776; +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Common/Constants/State.php b/rackspace/php-opencloud/lib/OpenCloud/Common/Constants/State.php index 3eb5f2c98..014cfaaf8 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Common/Constants/State.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Common/Constants/State.php @@ -1,21 +1,26 @@ - * @author Glen Campbell + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Common\Constants; - -class State +class State { const ACTIVE = 'ACTIVE'; - const ERROR = 'ERROR'; - + const ERROR = 'ERROR'; const DEFAULT_TIMEOUT = 3600; const DEFAULT_INTERVAL = 10; -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/AsyncError.php b/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/AsyncError.php index cbbacff38..489a333f7 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/AsyncError.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/AsyncError.php @@ -1,5 +1,22 @@ getResponse(); + + $message = sprintf( + "This operation was forbidden; the API returned a %s status code with this message:\n%s", + $response->getStatusCode(), + (string) $response->getBody() + ); + + $e = new self($message); + $e->setResponse($response); + $e->setRequest($exception->getRequest()); + + return $e; + } +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/HttpError.php b/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/HttpError.php index 1b54b8a82..c8376e810 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/HttpError.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/HttpError.php @@ -1,5 +1,22 @@ request = $request; + + return $this; + } + + /** + * Get the request that caused the exception + * + * @return RequestInterface + */ + public function getRequest() + { + return $this->request; + } + + /** + * Set the response that caused the exception + * + * @param Response $response Response to set + */ + public function setResponse(Response $response) + { + $this->response = $response; + } + + /** + * Get the response that caused the exception + * + * @return Response + */ + public function getResponse() + { + return $this->response; + } +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/HttpRetryError.php b/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/HttpRetryError.php index 78345840b..fd99fb3f9 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/HttpRetryError.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/HttpRetryError.php @@ -1,5 +1,22 @@ + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Common\Exceptions; @@ -13,4 +21,4 @@ class LoggingException extends Exception { -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/MetadataCreateError.php b/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/MetadataCreateError.php index a11939739..b5005306c 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/MetadataCreateError.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/MetadataCreateError.php @@ -1,5 +1,22 @@ + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Common\Exceptions; - class ResourceBucketException extends \Exception { - -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/ResourceNotFoundException.php b/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/ResourceNotFoundException.php new file mode 100644 index 000000000..689b6230d --- /dev/null +++ b/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/ResourceNotFoundException.php @@ -0,0 +1,25 @@ +getResponse(); + + $message = sprintf( + "This resource you were looking for could not be found; the API returned a %s status code with this message:\n%s", + $response->getStatusCode(), + (string) $response->getBody() + ); + + $e = new self($message); + $e->setResponse($response); + $e->setRequest($exception->getRequest()); + + return $e; + } +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/RuntimeException.php b/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/RuntimeException.php index fca33e023..c0c917f39 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/RuntimeException.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Common/Exceptions/RuntimeException.php @@ -1,5 +1,22 @@ + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Common\Http; use Guzzle\Http\Client as GuzzleClient; -use Guzzle\Http\Curl\CurlHandle; -use Guzzle\Http\Curl\CurlVersion; -use OpenCloud\Common\Exceptions\UnsupportedVersionError; +use OpenCloud\Version; /** * Base client object which handles HTTP transactions. Each service is based off of a Client which acts as a * centralized parent. - */ + */ class Client extends GuzzleClient { - const VERSION = '1.9.0'; - const MINIMUM_PHP_VERSION = '5.3.0'; - - public function __construct($baseUrl = '', $config = null) - { - // @codeCoverageIgnoreStart - if (PHP_VERSION < self::MINIMUM_PHP_VERSION) { - throw new UnsupportedVersionError(sprintf( - 'You must have PHP version >= %s installed.', - self::MINIMUM_PHP_VERSION - )); - } - // @codeCoverageIgnoreEnd - - parent::__construct($baseUrl, $config); - } - public function getDefaultUserAgent() { - return 'OpenCloud/' . self::VERSION - . ' cURL/' . CurlVersion::getInstance()->get('version') - . ' PHP/' . PHP_VERSION; + return 'OpenCloud/' . Version::getVersion() + . ' Guzzle/' . Version::getGuzzleVersion() + . ' cURL/' . Version::getCurlVersion() + . ' PHP/' . PHP_VERSION; } public function getUserAgent() { return $this->userAgent; } - -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Common/Http/Message/Formatter.php b/rackspace/php-opencloud/lib/OpenCloud/Common/Http/Message/Formatter.php index e6326f77c..1f2b8c2a4 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Common/Http/Message/Formatter.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Common/Http/Message/Formatter.php @@ -1,10 +1,18 @@ + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Common\Http\Message; @@ -16,13 +24,13 @@ class Formatter { - public static function decode(Response $response) { if (strpos($response->getHeader(Header::CONTENT_TYPE), Mime::JSON) !== false) { - $string = (string) $response->getBody(); + $string = (string) $response->getBody(); $response = json_decode($string); self::checkJsonError($string); + return $response; } } @@ -35,10 +43,9 @@ public static function encode($body) public static function checkJsonError($string = null) { if (json_last_error()) { - $error = sprintf('%s', json_last_error_msg()); + $error = sprintf('%s', json_last_error_msg()); $message = ($string) ? sprintf('%s trying to decode: %s', $error, $string) : $error; throw new JsonError($message); } } - -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Common/Http/Message/RequestSubscriber.php b/rackspace/php-opencloud/lib/OpenCloud/Common/Http/Message/RequestSubscriber.php index cd44038f9..788f67248 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Common/Http/Message/RequestSubscriber.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Common/Http/Message/RequestSubscriber.php @@ -1,18 +1,22 @@ - * @author Glen Campbell + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Common\Http\Message; -use Guzzle\Common\Event; -use Guzzle\Http\Message\EntityEnclosingRequest; -use OpenCloud\Common\Constants\Header; use Symfony\Component\EventDispatcher\EventSubscriberInterface; /** @@ -20,12 +24,11 @@ */ class RequestSubscriber implements EventSubscriberInterface { - public static function getInstance() { return new self(); } - + public static function getSubscribedEvents() { return array( @@ -41,17 +44,9 @@ public static function getSubscribedEvents() public function doCurlProgress($options) { $curlOptions = $options['request']->getCurlOptions(); - if ($curlOptions->hasKey('progressCallback')) { - return call_user_func($curlOptions->get('progressCallback')); - } else { - echo sprintf( - "Download size: [%d]\nDownloaded: [%d]\nUpload size: [%d]\nUploaded: [%d]\n", - $options['download_size'], - $options['downloaded'], - $options['upload_size'], - $options['uploaded'] - ); - } + + if ($curlOptions->hasKey('progressCallback')) { + return call_user_func($curlOptions->get('progressCallback')); + } } - -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Common/Lang.php b/rackspace/php-opencloud/lib/OpenCloud/Common/Lang.php index c147716c9..7c124e5bc 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Common/Lang.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Common/Lang.php @@ -1,29 +1,35 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Common; -class Lang +class Lang { - - public static function translate($word = null) - { - return $word; - } - - public static function noslash($str) - { - while ($str && (substr($str, -1) == '/')) { - $str = substr($str, 0, strlen($str) - 1); - } - return $str; - } - + public static function translate($word = null) + { + return $word; + } + + public static function noslash($str) + { + while ($str && (substr($str, -1) == '/')) { + $str = substr($str, 0, strlen($str) - 1); + } + + return $str; + } } diff --git a/rackspace/php-opencloud/lib/OpenCloud/Common/Log/AbstractLogger.php b/rackspace/php-opencloud/lib/OpenCloud/Common/Log/AbstractLogger.php deleted file mode 100644 index 7e7a44648..000000000 --- a/rackspace/php-opencloud/lib/OpenCloud/Common/Log/AbstractLogger.php +++ /dev/null @@ -1,140 +0,0 @@ -log(LogLevel::EMERGENCY, $message, $context); - } - - /** - * Action must be taken immediately. - * - * Example: Entire website down, database unavailable, etc. This should - * trigger the SMS alerts and wake you up. - * - * @param string $message - * @param array $context - * @return null - */ - public function alert($message, array $context = array()) - { - $this->log(LogLevel::ALERT, $message, $context); - } - - /** - * Critical conditions. - * - * Example: Application component unavailable, unexpected exception. - * - * @param string $message - * @param array $context - * @return null - */ - public function critical($message, array $context = array()) - { - $this->log(LogLevel::CRITICAL, $message, $context); - } - - /** - * Runtime errors that do not require immediate action but should typically - * be logged and monitored. - * - * @param string $message - * @param array $context - * @return null - */ - public function error($message, array $context = array()) - { - $this->log(LogLevel::ERROR, $message, $context); - } - - /** - * Exceptional occurrences that are not errors. - * - * Example: Use of deprecated APIs, poor use of an API, undesirable things - * that are not necessarily wrong. - * - * @param string $message - * @param array $context - * @return null - */ - public function warning($message, array $context = array()) - { - $this->log(LogLevel::WARNING, $message, $context); - } - - /** - * Normal but significant events. - * - * @param string $message - * @param array $context - * @return null - */ - public function notice($message, array $context = array()) - { - $this->log(LogLevel::NOTICE, $message, $context); - } - - /** - * Interesting events. - * - * Example: User logs in, SQL logs. - * - * @param string $message - * @param array $context - * @return null - */ - public function info($message, array $context = array()) - { - $this->log(LogLevel::INFO, $message, $context); - } - - /** - * Detailed debug information. - * - * @param string $message - * @param array $context - * @return null - */ - public function debug($message, array $context = array()) - { - $this->log(LogLevel::DEBUG, $message, $context); - } -} \ No newline at end of file diff --git a/rackspace/php-opencloud/lib/OpenCloud/Common/Log/LogLevel.php b/rackspace/php-opencloud/lib/OpenCloud/Common/Log/LogLevel.php deleted file mode 100644 index 5ccffd31b..000000000 --- a/rackspace/php-opencloud/lib/OpenCloud/Common/Log/LogLevel.php +++ /dev/null @@ -1,38 +0,0 @@ - + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Common\Log; use OpenCloud\Common\Exceptions\LoggingException; +use Psr\Log\AbstractLogger; +use Psr\Log\LogLevel; /** * Basic logger for OpenCloud which extends FIG's PSR-3 standard logger. - * + * * @link https://github.com/php-fig/log */ class Logger extends AbstractLogger -{ +{ /** * Is this debug class enabled or not? - * + * * @var bool */ private $enabled; - + /** - * These are the levels which will always be outputted - regardless of + * These are the levels which will always be outputted - regardless of * user-imposed settings. - * - * @var array + * + * @var array */ private $urgentLevels = array( LogLevel::EMERGENCY, LogLevel::ALERT, LogLevel::CRITICAL ); - + /** * Logging options. - * + * * @var array */ private $options = array( @@ -61,7 +71,7 @@ public static function newInstance() /** * Determines whether a log level needs to be outputted. - * + * * @param string $logLevel * @return bool */ @@ -69,12 +79,12 @@ private function outputIsUrgent($logLevel) { return in_array($logLevel, $this->urgentLevels); } - + /** * Interpolates context values into the message placeholders. - * + * * @param string $message - * @param array $context + * @param array $context * @return type */ private function interpolate($message, array $context = array()) @@ -88,32 +98,33 @@ private function interpolate($message, array $context = array()) // interpolate replacement values into the message and return return strtr($message, $replace); } - + /** * Enable or disable the debug class. - * + * * @param bool $enabled * @return self */ public function setEnabled($enabled) { $this->enabled = $enabled; + return $this; } - + /** * Is the debug class enabled? - * + * * @return bool */ public function isEnabled() { return $this->enabled === true; } - + /** * Set an array of options. - * + * * @param array $options */ public function setOptions(array $options = array()) @@ -121,22 +132,23 @@ public function setOptions(array $options = array()) foreach ($options as $key => $value) { $this->setOption($key, $value); } + return $this; } - + /** * Get all options. - * + * * @return array */ public function getOptions() { return $this->options; } - + /** * Set an individual option. - * + * * @param string $key * @param string $value */ @@ -144,13 +156,14 @@ public function setOption($key, $value) { if ($this->optionExists($key)) { $this->options[$key] = $value; + return $this; } } - + /** * Get an individual option. - * + * * @param string $key * @return string|null */ @@ -160,10 +173,10 @@ public function getOption($key) return $this->options[$key]; } } - + /** * Check whether an individual option exists. - * + * * @param string $key * @return bool */ @@ -171,10 +184,10 @@ private function optionExists($key) { return array_key_exists($key, $this->getOptions()); } - + /** * Outputs a log message if necessary. - * + * * @param string $logLevel * @param string $message * @param string $context @@ -185,30 +198,31 @@ public function log($level, $message, array $context = array()) $this->dispatch($message, $context); } } - + /** * Used to format the line outputted in the log file. - * + * * @param string $string * @return string */ private function formatFileLine($string) { $format = $this->getOption('dateFormat') . $this->getOption('delimeter'); + return date($format) . $string; } - + /** * Dispatch a log output message. - * + * * @param string $message - * @param array $context + * @param array $context * @throws LoggingException */ private function dispatch($message, $context) { $output = $this->interpolate($message, $context) . PHP_EOL; - + if ($this->getOption('outputToFile') === true) { $file = $this->getOption('logFile'); @@ -217,19 +231,20 @@ private function dispatch($message, $context) 'The log file either does not exist or is not writeable' ); } - + // Output to file file_put_contents($file, $this->formatFileLine($output), FILE_APPEND); } else { - echo $output; } } - public function deprecated($method, $new) + /** + * Helper method, use PSR-3 warning function for deprecation warnings + * @see http://www.php-fig.org/psr/psr-3/ + */ + public static function deprecated($method, $new) { - $string = sprintf('The %s method is deprecated, please use %s instead', $method, $new); - return $this->warning($string); + return sprintf('The %s method is deprecated, please use %s instead', $method, $new); } - -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Common/Log/LoggerInterface.php b/rackspace/php-opencloud/lib/OpenCloud/Common/Log/LoggerInterface.php deleted file mode 100644 index ab95a11bb..000000000 --- a/rackspace/php-opencloud/lib/OpenCloud/Common/Log/LoggerInterface.php +++ /dev/null @@ -1,134 +0,0 @@ - - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Common; @@ -18,7 +25,7 @@ class Metadata extends Base implements \Countable /** * @var array Internal data store. */ - protected $metadata = array(); + protected $metadata = array(); /** * This setter overrides the base one, since the metadata key can be @@ -28,7 +35,7 @@ class Metadata extends Base implements \Countable * @param string $value * @return void */ - public function __set($property, $value) + public function __set($property, $value) { return $this->setProperty($property, $value); } @@ -37,34 +44,34 @@ public function __get($key) { return $this->getProperty($key); } - + public function propertyExists($property, $allowRetry = true) { - return isset($this->metadata[strtolower($property)]) - || parent::propertyExists($property, $allowRetry); + return isset($this->metadata[strtolower($property)]) + || parent::propertyExists($property, $allowRetry); } - + public function getProperty($property) { return $this->propertyExists($property) ? $this->metadata[strtolower($property)] : null; } - + public function setProperty($property, $value) { $this->metadata[strtolower($property)] = $value; } - + public function __isset($property) { return $this->propertyExists($property); } - + /** * Returns the list of keys defined * * @return array */ - public function keylist() + public function keylist() { return $this->metadata; } @@ -75,26 +82,26 @@ public function keylist() * If $prefix is provided, then only array keys that match the prefix * are set as metadata values, and $prefix is stripped from the key name. * - * @param array $values an array of key/value pairs to set + * @param array $values an array of key/value pairs to set * @param string $prefix if provided, a prefix that is used to identify - * metadata values. For example, you can set values from headers - * for a Container by using $prefix='X-Container-Meta-'. + * metadata values. For example, you can set values from headers + * for a Container by using $prefix='X-Container-Meta-'. * @return void */ - public function setArray($values, $prefix = null) + public function setArray($values, $prefix = null) { if (empty($values)) { return false; } - + foreach ($values as $key => $value) { if ($prefix && strpos($key, $prefix) === 0) { $key = substr($key, strlen($prefix)); - } + } $this->setProperty($key, $value); } } - + public function toArray() { return $this->metadata; @@ -104,5 +111,4 @@ public function count() { return count($this->metadata); } - } diff --git a/rackspace/php-opencloud/lib/OpenCloud/Common/PersistentObject.php b/rackspace/php-opencloud/lib/OpenCloud/Common/PersistentObject.php index e9a9dc4c7..8176ce228 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Common/PersistentObject.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Common/PersistentObject.php @@ -1,734 +1,34 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Common; -use Guzzle\Http\Exception\BadResponseException; -use Guzzle\Http\Message\Response; -use Guzzle\Http\Url; -use OpenCloud\Common\Constants\State as StateConst; -use OpenCloud\Common\Service\ServiceInterface; -use OpenCloud\Common\Exceptions\RuntimeException; -use OpenCloud\Common\Http\Message\Formatter; +use OpenCloud\Common\Resource\PersistentResource; /** - * Represents an object that can be retrieved, created, updated and deleted. - * - * This class abstracts much of the common functionality between: - * - * * Nova servers; - * * Swift containers and objects; - * * DBAAS instances; - * * Cinder volumes; - * * and various other objects that: - * * have a URL; - * * can be created, updated, deleted, or retrieved; - * * use a standard JSON format with a top-level element followed by - * a child object with attributes. - * - * In general, you can create a persistent object class by subclassing this - * class and defining some protected, static variables: - * - * * $url_resource - the sub-resource value in the URL of the parent. For - * example, if the parent URL is `http://something/parent`, then setting this - * value to "another" would result in a URL for the persistent object of - * `http://something/parent/another`. - * - * * $json_name - the top-level JSON object name. For example, if the - * persistent object is represented by `{"foo": {"attr":value, ...}}`, then - * set $json_name to "foo". - * - * * $json_collection_name - optional; this value is the name of a collection - * of the persistent objects. If not provided, it defaults to `json_name` - * with an appended "s" (e.g., if `json_name` is "foo", then - * `json_collection_name` would be "foos"). Set this value if the collection - * name doesn't follow this pattern. - * - * * $json_collection_element - the common pattern for a collection is: - * `{"collection": [{"attr":"value",...}, {"attr":"value",...}, ...]}` - * That is, each element of the array is a \stdClass object containing the - * object's attributes. In rare instances, the objects in the array - * are named, and `json_collection_element` contains the name of the - * collection objects. For example, in this JSON response: - * `{"allowedDomain":[{"allowedDomain":{"name":"foo"}}]}`, - * `json_collection_element` would be set to "allowedDomain". + * This class is deprecated; its functionality has been split out into the following classes: * - * The PersistentObject class supports the standard CRUD methods; if these are - * not needed (i.e. not supported by the service), the subclass should redefine - * these to call the `noCreate`, `noUpdate`, or `noDelete` methods, which will - * trigger an appropriate exception. For example, if an object cannot be created: + * * {@see \OpenCloud\Common\Resource\BaseResource} + * * {@see \OpenCloud\Common\Resource\NovaResource} + * * {@see \OpenCloud\Common\Resource\PersistentResource} * - * function create($params = array()) - * { - * $this->noCreate(); - * } + * @deprecated + * @package OpenCloud\Common */ -abstract class PersistentObject extends Base +abstract class PersistentObject extends PersistentResource { - private $service; - private $parent; - protected $metadata; - - /** - * Retrieves the instance from persistent storage - * - * @param mixed $service The service object for this resource - * @param mixed $info The ID or array/object of data - */ - public function __construct($service = null, $info = null) - { - if ($service instanceof ServiceInterface) { - $this->setService($service); - } - - $this->metadata = new Metadata; - - $this->populate($info); - } - - /** - * Sets the service associated with this resource object. - * - * @param \OpenCloud\Common\Service\ServiceInterface $service - * @return \OpenCloud\Common\PersistentObject - */ - public function setService(ServiceInterface $service) - { - $this->service = $service; - return $this; - } - - /** - * Returns the service object for this resource; required for making - * requests, etc. because it has direct access to the Connection. - * - * @return \OpenCloud\Common\Service\ServiceInterface - * @throws \OpenCloud\Common\Exceptions\ServiceException - */ - public function getService() - { - if (null === $this->service) { - throw new Exceptions\ServiceException( - 'No service defined' - ); - } - return $this->service; - } - - /** - * Set the parent object for this resource. - * - * @param \OpenCloud\Common\PersistentObject $parent - * @return \OpenCloud\Common\PersistentObject - */ - public function setParent(PersistentObject $parent) - { - $this->parent = $parent; - return $this; - } - - /** - * Returns the parent. - * - * @return \OpenCloud\Common\PersistentObject - */ - public function getParent() - { - if (null === $this->parent) { - $this->parent = $this->getService(); - } - return $this->parent; - } - - public function getClient() - { - return $this->getService()->getClient(); - } - - public function setMetadata($metadata) - { - $this->metadata = $metadata; - - return $this; - } - - public function getMetadata() - { - return $this->metadata; - } - - /** - * Creates a new object - * - * @param array $params array of values to set when creating the object - * @return HttpResponse - * @throws VolumeCreateError if HTTP status is not Success - */ - public function create($params = array()) - { - // set parameters - if (!empty($params)) { - $this->populate($params, false); - } - - // construct the JSON - $json = json_encode($this->createJson()); - $this->checkJsonError(); - - $createUrl = $this->createUrl(); - - $response = $this->getClient()->post($createUrl, self::getJsonHeader(), $json)->send(); - - // We have to try to parse the response body first because it should have precedence over a Location refresh. - // I'd like to reverse the order, but Nova instances return ephemeral properties on creation which are not - // available when you follow the Location link... - if (null !== ($decoded = $this->parseResponse($response))) { - $this->populate($decoded); - } elseif ($location = $response->getHeader('Location')) { - $this->refreshFromLocationUrl($location); - } - - return $response; - } - - public function refreshFromLocationUrl($url) - { - $fullUrl = Url::factory($url); - - $response = $this->getClient()->get($fullUrl)->send(); - - if (null !== ($decoded = $this->parseResponse($response))) { - $this->populate($decoded); - } - } - - /** - * Updates an existing object - * - * @api - * @param array $params array of values to set when updating the object - * @return HttpResponse - * @throws VolumeCreateError if HTTP status is not Success - */ - public function update($params = array()) - { - // set parameters - if (!empty($params)) { - $this->populate($params); - } - - // debug - $this->getLogger()->info('{class}::Update({name})', array( - 'class' => get_class($this), - 'name' => $this->getProperty($this->primaryKeyField()) - )); - - // construct the JSON - $json = json_encode($this->updateJson($params)); - $this->checkJsonError(); - - // send the request - return $this->getClient()->put($this->getUrl(), self::getJsonHeader(), $json)->send(); - } - - /** - * Refreshes the object from the origin (useful when the server is - * changing states) - * - * @param string|null $id - * @param string|null $url - * - * @return void - * @throws Exceptions\IdRequiredError - */ - public function refresh($id = null, $url = null) - { - $primaryKey = $this->primaryKeyField(); - $primaryKeyVal = $this->getProperty($primaryKey); - - if (!$url) { - - if (!$id = $id ?: $primaryKeyVal) { - throw new Exceptions\IdRequiredError(sprintf( - Lang::translate("%s has no %s; cannot be refreshed"), - get_class($this), - $primaryKey - )); - } - - if ($primaryKeyVal != $id) { - $this->setProperty($primaryKey, $id); - } - - $url = $this->getUrl(); - } - - // reset status, if available - if ($this->getProperty('status')) { - $this->setProperty('status', null); - } - - $response = $this->getClient()->get($url)->send(); - - if (null !== ($decoded = $this->parseResponse($response))) { - $this->populate($decoded); - } - - return $response; - } - - /** - * Deletes an object - * - * @api - * @return HttpResponse - * @throws DeleteError if HTTP status is not Success - */ - public function delete() - { - $this->getLogger()->info('{class}::Delete()', array('class' => get_class($this))); - - // send the request - return $this->getClient()->delete($this->getUrl())->send(); - } - - /** - * @deprecated - */ - public function url($path = null, array $query = array()) - { - return $this->getUrl($path, $query); - } - - /** - * Returns the default URL of the object - * - * This may have to be overridden in subclasses. - * - * @param string $subresource optional sub-resource string - * @param array $qstr optional k/v pairs for query strings - * @return string - */ - public function getUrl($path = null, array $query = array()) - { - if (!$url = $this->findLink('self')) { - - // ...otherwise construct a URL from parent and this resource's - // "URL name". If no name is set, resourceName() throws an error. - $url = $this->getParent()->getUrl($this->resourceName()); - - // Does it have a primary key? - if (null !== ($primaryKey = $this->getProperty($this->primaryKeyField()))) { - $url->addPath((string) $primaryKey); - } - } - - if (!$url instanceof Url) { - $url = Url::factory($url); - } - - return $url->addPath((string) $path)->setQuery($query); - } - - /** - * Waits for the server/instance status to change - * - * This function repeatedly polls the system for a change in server - * status. Once the status reaches the `$terminal` value (or 'ERROR'), - * then the function returns. - * - * @api - * @param string $terminal the terminal state to wait for - * @param integer $timeout the max time (in seconds) to wait - * @param callable $callback a callback function that is invoked with - * each repetition of the polling sequence. This can be used, for - * example, to update a status display or to permit other operations - * to continue - * @return void - * @codeCoverageIgnore - */ - public function waitFor($state = null, $timeout = null, $callback = null, $interval = null) - { - $state = $state ?: StateConst::ACTIVE; - $timeout = $timeout ?: StateConst::DEFAULT_TIMEOUT; - $interval = $interval ?: StateConst::DEFAULT_INTERVAL; - - // save stats - $startTime = time(); - - $states = array('ERROR', $state); - - while (true) { - - $this->refresh($this->getProperty($this->primaryKeyField())); - - if ($callback) { - call_user_func($callback, $this); - } - - if (in_array($this->status(), $states) || (time() - $startTime) > $timeout) { - return; - } - - sleep($interval); - } - } - - /** - * Sends the json string to the /action resource - * - * This is used for many purposes, such as rebooting the server, - * setting the root password, creating images, etc. - * Since it can only be used on a live server, it checks for a valid ID. - * - * @param $object - this will be encoded as json, and we handle all the JSON - * error-checking in one place - * @throws Exceptions\IdRequiredError if server ID is not defined - * @throws Exceptions\ServerActionError on other errors - * @returns boolean; TRUE if successful, FALSE otherwise - */ - protected function action($object) - { - if (!$this->getProperty($this->primaryKeyField())) { - throw new Exceptions\IdRequiredError(sprintf( - Lang::translate('%s is not defined'), - get_class($this), - $this->primaryKeyField() - )); - } - - if (!is_object($object)) { - throw new Exceptions\ServerActionError(sprintf( - Lang::translate('%s::Action() requires an object as its parameter'), - get_class($this) - )); - } - - // convert the object to json - $json = json_encode($object); - $this->checkJsonError(); - - // debug - save the request - $this->getLogger()->info(Lang::translate('{class}::action [{json}]'), array( - 'class' => get_class($this), - 'json' => $json - )); - - // get the URL for the POST message - $url = $this->url('action'); - - // POST the message - return $this->getClient()->post($url, self::getJsonHeader(), $json)->send(); - } - - /** - * Returns an object for the Create() method JSON - * Must be overridden in a child class. - * - * @throws CreateError if not overridden - */ - protected function createJson() - { - if (!isset($this->createKeys)) { - throw new RuntimeException(sprintf( - 'This resource object [%s] must have a visible createKeys array', - get_class($this) - )); - } - - $element = (object) array(); - - foreach ($this->createKeys as $key) { - if (null !== ($property = $this->getProperty($key))) { - $element->$key = $property; - } - } - - if (isset($this->metadata) && count($this->metadata)) { - $element->metadata = (object) $this->metadata->toArray(); - } - - return (object) array($this->jsonName() => (object) $element); - } - - /** - * Returns an object for the Update() method JSON - * Must be overridden in a child class. - * - * @throws Exceptions\UpdateError if not overridden - */ - protected function updateJson($params = array()) - { - throw new Exceptions\UpdateError(sprintf( - Lang::translate('[%s] UpdateJson() must be overridden'), - get_class($this) - )); - } - - /** - * throws a CreateError for subclasses that don't support Create - * - * @throws Exceptions\CreateError - */ - protected function noCreate() - { - throw new Exceptions\CreateError(sprintf( - Lang::translate('[%s] does not support Create()'), - get_class($this) - )); - } - - /** - * throws a DeleteError for subclasses that don't support Delete - * - * @throws Exceptions\DeleteError - */ - protected function noDelete() - { - throw new Exceptions\DeleteError(sprintf( - Lang::translate('[%s] does not support Delete()'), - get_class($this) - )); - } - - /** - * throws a UpdateError for subclasses that don't support Update - * - * @throws Exceptions\UpdateError - */ - protected function noUpdate() - { - throw new Exceptions\UpdateError(sprintf( - Lang::translate('[%s] does not support Update()'), - get_class($this) - )); - } - - /** - * Returns the displayable name of the object - * - * Can be overridden by child objects; *must* be overridden by child - * objects if the object does not have a `name` attribute defined. - * - * @api - * @return string - * @throws Exceptions\NameError if attribute 'name' is not defined - */ - public function name() - { - if (null !== ($name = $this->getProperty('name'))) { - return $name; - } else { - throw new Exceptions\NameError(sprintf( - Lang::translate('Name attribute does not exist for [%s]'), - get_class($this) - )); - } - } - - /** - * returns the object's status or `N/A` if not available - * - * @api - * @return string - */ - public function status() - { - return (isset($this->status)) ? $this->status : 'N/A'; - } - - /** - * returns the object's identifier - * - * Can be overridden by a child class if the identifier is not in the - * `$id` property. Use of this function permits the `$id` attribute to - * be protected or private to prevent unauthorized overwriting for - * security. - * - * @api - * @return string - */ - public function id() - { - return $this->id; - } - - /** - * checks for `$alias` in extensions and throws an error if not present - * - * @throws Exceptions\UnsupportedExtensionError - */ - public function checkExtension($alias) - { - if (!in_array($alias, $this->getService()->namespaces())) { - throw new Exceptions\UnsupportedExtensionError(sprintf( - Lang::translate('Extension [%s] is not installed'), - $alias - )); - } - - return true; - } - - /** - * returns the region associated with the object - * - * navigates to the parent service to determine the region. - * - * @api - */ - public function region() - { - return $this->getService()->Region(); - } - - /** - * Since each server can have multiple links, this returns the desired one - * - * @param string $type - 'self' is most common; use 'bookmark' for - * the version-independent one - * @return string the URL from the links block - */ - public function findLink($type = 'self') - { - if (empty($this->links)) { - return false; - } - - foreach ($this->links as $link) { - if ($link->rel == $type) { - return $link->href; - } - } - - return false; - } - - /** - * returns the URL used for Create - * - * @return string - */ - public function createUrl() - { - return $this->getParent()->getUrl($this->resourceName()); - } - - /** - * Returns the primary key field for the object - * - * The primary key is usually 'id', but this function is provided so that - * (in rare cases where it is not 'id'), it can be overridden. - * - * @return string - */ - protected function primaryKeyField() - { - return 'id'; - } - - /** - * Returns the top-level document identifier for the returned response - * JSON document; must be overridden in child classes - * - * For example, a server document is (JSON) `{"server": ...}` and an - * Instance document is `{"instance": ...}` - this function must return - * the top level document name (either "server" or "instance", in - * these examples). - * - * @throws Exceptions\DocumentError if not overridden - */ - public static function jsonName() - { - if (isset(static::$json_name)) { - return static::$json_name; - } - - throw new Exceptions\DocumentError(sprintf( - Lang::translate('No JSON object defined for class [%s] in JsonName()'), - get_class() - )); - } - - /** - * returns the collection JSON element name - * - * When an object is returned in a collection, it usually has a top-level - * object that is an array holding child objects of the object types. - * This static function returns the name of the top-level element. Usually, - * that top-level element is simply the JSON name of the resource.'s'; - * however, it can be overridden by specifying the $json_collection_name - * attribute. - * - * @return string - */ - public static function jsonCollectionName() - { - if (isset(static::$json_collection_name)) { - return static::$json_collection_name; - } else { - return static::$json_name . 's'; - } - } - - /** - * returns the JSON name for each element in a collection - * - * Usually, elements in a collection are anonymous; this function, however, - * provides for an element level name: - * - * `{ "collection" : [ { "element" : ... } ] }` - * - * @return string - */ - public static function jsonCollectionElement() - { - if (isset(static::$json_collection_element)) { - return static::$json_collection_element; - } - } - - /** - * Returns the resource name for the URL of the object; must be overridden - * in child classes - * - * For example, a server is `/servers/`, a database instance is - * `/instances/`. Must be overridden in child classes. - * - * @throws Exceptions\UrlError - */ - public static function resourceName() - { - if (isset(static::$url_resource)) { - return static::$url_resource; - } - - throw new Exceptions\UrlError(sprintf( - Lang::translate('No URL resource defined for class [%s] in ResourceName()'), - get_class() - )); - } - - public function parseResponse(Response $response) - { - $body = Formatter::decode($response); - - $top = $this->jsonName(); - - if ($top && isset($body->$top)) { - $content = $body->$top; - } else { - $content = $body; - } - - return $content; - } - } diff --git a/rackspace/php-opencloud/lib/OpenCloud/Common/Resource/BaseResource.php b/rackspace/php-opencloud/lib/OpenCloud/Common/Resource/BaseResource.php new file mode 100644 index 000000000..76c48cf10 --- /dev/null +++ b/rackspace/php-opencloud/lib/OpenCloud/Common/Resource/BaseResource.php @@ -0,0 +1,294 @@ +setService($service); + $this->metadata = new Metadata(); + $this->populate($data); + } + + /** + * @param \OpenCloud\Common\Service\ServiceInterface $service + * @return \OpenCloud\Common\PersistentObject + */ + public function setService(ServiceInterface $service) + { + $this->service = $service; + + return $this; + } + + /** + * @return \OpenCloud\Common\Service\ServiceInterface + * @throws \OpenCloud\Common\Exceptions\ServiceException + */ + public function getService() + { + if (null === $this->service) { + throw new ServiceException('No service defined'); + } + + return $this->service; + } + + /** + * @param BaseResource $parent + * @return self + */ + public function setParent(BaseResource $parent) + { + $this->parent = $parent; + + return $this; + } + + /** + * @return mixed + */ + public function getParent() + { + if (null === $this->parent) { + $this->parent = $this->getService(); + } + + return $this->parent; + } + + /** + * Convenience method to return the service's client + * + * @return \Guzzle\Http\ClientInterface + */ + public function getClient() + { + return $this->getService()->getClient(); + } + + /** + * @param mixed $metadata + * @return $this + */ + public function setMetadata($data) + { + if ($data instanceof Metadata) { + $metadata = $data; + } elseif (is_array($data) || is_object($data)) { + $metadata = new Metadata(); + $metadata->setArray($data); + } else { + throw new \InvalidArgumentException(sprintf( + 'You must specify either an array/object of parameters, or an ' + . 'instance of Metadata. You provided: %s', + print_r($data, true) + )); + } + + $this->metadata = $metadata; + + return $this; + } + + /** + * @return Metadata + */ + public function getMetadata() + { + return $this->metadata; + } + + /** + * Get this resource's URL + * + * @param null $path URI path to add on + * @param array $query Query to add on + * @return mixed + */ + public function getUrl($path = null, array $query = array()) + { + if (!$url = $this->findLink('self')) { + // ...otherwise construct a URL from parent and this resource's + // "URL name". If no name is set, resourceName() throws an error. + $url = $this->getParent()->getUrl($this->resourceName()); + + // Does it have a primary key? + if (null !== ($primaryKey = $this->getProperty($this->primaryKeyField()))) { + $url->addPath((string) $primaryKey); + } + } + + if (!$url instanceof Url) { + $url = Url::factory($url); + } + + return $url->addPath((string) $path)->setQuery($query); + } + + /** + * @deprecated + */ + public function url($path = null, array $query = array()) + { + return $this->getUrl($path, $query); + } + + + /** + * Find a resource link based on a type + * + * @param string $type + * @return bool + */ + public function findLink($type = 'self') + { + if (empty($this->links)) { + return false; + } + + foreach ($this->links as $link) { + if ($link->rel == $type) { + return $link->href; + } + } + + return false; + } + + /** + * Returns the primary key field for the object + * + * @return string + */ + protected function primaryKeyField() + { + return 'id'; + } + + /** + * Returns the top-level key for the returned response JSON document + * + * @throws DocumentError + */ + public static function jsonName() + { + if (isset(static::$json_name)) { + return static::$json_name; + } + + throw new DocumentError('A top-level JSON document key has not been defined for this resource'); + } + + /** + * Returns the top-level key for collection responses + * + * @return string + */ + public static function jsonCollectionName() + { + return isset(static::$json_collection_name) ? static::$json_collection_name : static::$json_name . 's'; + } + + /** + * Returns the nested keys that could (rarely) prefix collection items. For example: + * + * { + * "keypairs": [ + * { + * "keypair": { + * "fingerprint": "...", + * "name": "key1", + * "public_key": "..." + * } + * }, + * { + * "keypair": { + * "fingerprint": "...", + * "name": "key2", + * "public_key": "..." + * } + * } + * ] + * } + * + * In the above example, "keypairs" would be the $json_collection_name and "keypair" would be the + * $json_collection_element + * + * @return string + */ + public static function jsonCollectionElement() + { + if (isset(static::$json_collection_element)) { + return static::$json_collection_element; + } + } + + /** + * Returns the URI path for this resource + * + * @throws UrlError + */ + public static function resourceName() + { + if (isset(static::$url_resource)) { + return static::$url_resource; + } + + throw new UrlError('No URL path defined for this resource'); + } + + /** + * Parse a HTTP response for the required content + * + * @param Response $response + * @return mixed + */ + public function parseResponse(Response $response) + { + $document = Formatter::decode($response); + + $topLevelKey = $this->jsonName(); + + return ($topLevelKey && isset($document->$topLevelKey)) ? $document->$topLevelKey : $document; + } +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Common/Resource/NovaResource.php b/rackspace/php-opencloud/lib/OpenCloud/Common/Resource/NovaResource.php new file mode 100644 index 000000000..ee76c0d86 --- /dev/null +++ b/rackspace/php-opencloud/lib/OpenCloud/Common/Resource/NovaResource.php @@ -0,0 +1,51 @@ +getProperty($this->primaryKeyField())) { + throw new \RuntimeException('A primary key is required'); + } + + if (!is_object($object)) { + throw new \InvalidArgumentException(sprintf('This method expects an object as its parameter')); + } + + // convert the object to json + $json = json_encode($object); + $this->checkJsonError(); + + // get the URL for the POST message + $url = clone $this->getUrl(); + $url->addPath('action'); + + // POST the message + return $this->getClient()->post($url, self::getJsonHeader(), $json)->send(); + } +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Common/Resource/PersistentResource.php b/rackspace/php-opencloud/lib/OpenCloud/Common/Resource/PersistentResource.php new file mode 100644 index 000000000..33aa1c398 --- /dev/null +++ b/rackspace/php-opencloud/lib/OpenCloud/Common/Resource/PersistentResource.php @@ -0,0 +1,440 @@ +populate($params, false); + } + + // construct the JSON + $json = json_encode($this->createJson()); + $this->checkJsonError(); + + $createUrl = $this->createUrl(); + + $response = $this->getClient()->post($createUrl, self::getJsonHeader(), $json)->send(); + + // We have to try to parse the response body first because it should have precedence over a Location refresh. + // I'd like to reverse the order, but Nova instances return ephemeral properties on creation which are not + // available when you follow the Location link... + if (null !== ($decoded = $this->parseResponse($response))) { + $this->populate($decoded); + } elseif ($location = $response->getHeader('Location')) { + $this->refreshFromLocationUrl($location); + } + + return $response; + } + + /** + * Update a resource + * + * @param array $params + * @return \Guzzle\Http\Message\Response + */ + public function update($params = array()) + { + // set parameters + if (!empty($params)) { + $this->populate($params); + } + + // construct the JSON + $json = json_encode($this->updateJson($params)); + $this->checkJsonError(); + + // send the request + return $this->getClient()->put($this->getUrl(), self::getJsonHeader(), $json)->send(); + } + + /** + * Delete this resource + * + * @return \Guzzle\Http\Message\Response + */ + public function delete() + { + return $this->getClient()->delete($this->getUrl())->send(); + } + + /** + * Refresh the state of a resource + * + * @param null $id + * @param null $url + * @return \Guzzle\Http\Message\Response + * @throws IdRequiredError + */ + public function refresh($id = null, $url = null) + { + $primaryKey = $this->primaryKeyField(); + $primaryKeyVal = $this->getProperty($primaryKey); + + if (!$url) { + if (!$id = $id ?: $primaryKeyVal) { + $message = sprintf("This resource cannot be refreshed because it has no %s", $primaryKey); + throw new IdRequiredError($message); + } + + if ($primaryKeyVal != $id) { + $this->setProperty($primaryKey, $id); + } + + $url = $this->getUrl(); + } + + // reset status, if available + if ($this->getProperty('status')) { + $this->setProperty('status', null); + } + + $response = $this->getClient()->get($url)->send(); + + if (null !== ($decoded = $this->parseResponse($response))) { + $this->populate($decoded); + } + + return $response; + } + + + /** + * Causes resource to refresh based on parent's URL + */ + protected function refreshFromParent() + { + $url = clone $this->getParent()->getUrl(); + $url->addPath($this->resourceName()); + + $response = $this->getClient()->get($url)->send(); + + if (null !== ($decoded = $this->parseResponse($response))) { + $this->populate($decoded); + } + } + + /** + * Given a `location` URL, refresh this resource + * + * @param $url + */ + public function refreshFromLocationUrl($url) + { + $fullUrl = Url::factory($url); + + $response = $this->getClient()->get($fullUrl)->send(); + + if (null !== ($decoded = $this->parseResponse($response))) { + $this->populate($decoded); + } + } + + /** + * A method to repeatedly poll the API resource, waiting for an eventual state change + * + * @param null $state The expected state of the resource + * @param null $timeout The maximum timeout to wait + * @param null $callback The callback to use to check the state + * @param null $interval How long between each refresh request + */ + public function waitFor($state = null, $timeout = null, $callback = null, $interval = null) + { + $state = $state ?: State::ACTIVE; + $timeout = $timeout ?: State::DEFAULT_TIMEOUT; + $interval = $interval ?: State::DEFAULT_INTERVAL; + + // save stats + $startTime = time(); + + $states = array('ERROR', $state); + + while (true) { + $this->refresh($this->getProperty($this->primaryKeyField())); + + if ($callback) { + call_user_func($callback, $this); + } + + if (in_array($this->status(), $states) || (time() - $startTime) > $timeout) { + return; + } + + sleep($interval); + } + } + + /** + * Provides JSON for create request body + * + * @return object + * @throws \RuntimeException + */ + protected function createJson() + { + if (!isset($this->createKeys)) { + throw new \RuntimeException(sprintf( + 'This resource object [%s] must have a visible createKeys array', + get_class($this) + )); + } + + $element = (object) array(); + + foreach ($this->createKeys as $key) { + if (null !== ($property = $this->getProperty($key))) { + $element->{$this->getAlias($key)} = $this->recursivelyAliasPropertyValue($property); + } + } + + if (isset($this->metadata) && count($this->metadata)) { + $element->metadata = (object) $this->metadata->toArray(); + } + + return (object) array($this->jsonName() => (object) $element); + } + + /** + * Returns the alias configured for the given key. If no alias exists + * it returns the original key. + * + * @param string $key + * @return string + */ + protected function getAlias($key) + { + if (false !== ($alias = array_search($key, $this->aliases))) { + return $alias; + } + + return $key; + } + + /** + * Returns the given property value's alias, if configured; Else, the + * unchanged property value is returned. If the given property value + * is an array or an instance of \stdClass, it is aliases recursively. + * + * @param mixed $propertyValue Array or \stdClass instance to alias + * @return mixed Property value, aliased recursively + */ + protected function recursivelyAliasPropertyValue($propertyValue) + { + if (is_array($propertyValue)) { + foreach ($propertyValue as $key => $subValue) { + $aliasedSubValue = $this->recursivelyAliasPropertyValue($subValue); + if (is_numeric($key)) { + $propertyValue[$key] = $aliasedSubValue; + } else { + unset($propertyValue[$key]); + $propertyValue[$this->getAlias($key)] = $aliasedSubValue; + } + } + } elseif (is_object($propertyValue) && ($propertyValue instanceof \stdClass)) { + foreach (get_object_vars($propertyValue) as $key => $subValue) { + unset($propertyValue->$key); + $propertyValue->{$this->getAlias($key)} = $this->recursivelyAliasPropertyValue($subValue); + } + } + + return $propertyValue; + } + + /** + * Provides JSON for update request body + */ + protected function updateJson($params = array()) + { + if (!isset($this->updateKeys)) { + throw new \RuntimeException(sprintf( + 'This resource object [%s] must have a visible updateKeys array', + get_class($this) + )); + } + + $element = (object) array(); + + foreach ($this->updateKeys as $key) { + if (null !== ($property = $this->getProperty($key))) { + $element->{$this->getAlias($key)} = $this->recursivelyAliasPropertyValue($property); + } + } + + return (object) array($this->jsonName() => (object) $element); + } + + /** + * @throws CreateError + */ + protected function noCreate() + { + throw new CreateError('This resource does not support the create operation'); + } + + /** + * @throws DeleteError + */ + protected function noDelete() + { + throw new DeleteError('This resource does not support the delete operation'); + } + + /** + * @throws UpdateError + */ + protected function noUpdate() + { + throw new UpdateError('This resource does not support the update operation'); + } + + /** + * Check whether an extension is valid + * + * @param mixed $alias The extension name + * @return bool + * @throws UnsupportedExtensionError + */ + public function checkExtension($alias) + { + if (!in_array($alias, $this->getService()->namespaces())) { + throw new UnsupportedExtensionError(sprintf("%s extension is not installed", $alias)); + } + + return true; + } + + /** + * Returns the object's properties as an array + */ + protected function getUpdateablePropertiesAsArray() + { + $properties = get_object_vars($this); + + $propertiesToKeep = array(); + foreach ($this->updateKeys as $key) { + if (isset($properties[$key])) { + $propertiesToKeep[$key] = $properties[$key]; + } + } + + return $propertiesToKeep; + } + + /** + * Generates a JSON Patch representation and return its + * + * @param mixed $updatedProperties Properties of the resource to update + * @return String JSON Patch representation for updates + */ + protected function generateJsonPatch($updatedProperties) + { + // Normalize current and updated properties into nested arrays + $currentProperties = json_decode(json_encode($this->getUpdateablePropertiesAsArray()), true); + $updatedProperties = json_decode(json_encode($updatedProperties), true); + + // Add any properties that haven't changed to generate the correct patch + // (otherwise unchanging properties are marked as removed in the patch) + foreach ($currentProperties as $key => $value) { + if (!array_key_exists($key, $updatedProperties)) { + $updatedProperties[$key] = $value; + } + } + + // Recursively alias current and updated properties + $currentProperties = $this->recursivelyAliasPropertyValue($currentProperties); + $updatedProperties = $this->recursivelyAliasPropertyValue($updatedProperties); + + // Generate JSON Patch representation + $json = json_encode(JsonPatch::diff($currentProperties, $updatedProperties)); + $this->checkJsonError(); + + return $json; + } + + /******** DEPRECATED METHODS ********/ + + /** + * @deprecated + * @return string + * @throws NameError + */ + public function name() + { + if (null !== ($name = $this->getProperty('name'))) { + return $name; + } else { + throw new NameError('Name attribute does not exist for this resource'); + } + } + + /** + * @deprecated + * @return mixed + */ + public function id() + { + return $this->id; + } + + /** + * @deprecated + * @return string + */ + public function status() + { + return (isset($this->status)) ? $this->status : 'N/A'; + } + + /** + * @deprecated + * @return mixed + */ + public function region() + { + return $this->getService()->region(); + } + + /** + * @deprecated + * @return \Guzzle\Http\Url + */ + public function createUrl() + { + return $this->getParent()->getUrl($this->resourceName()); + } +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Common/Resource/ReadOnlyResource.php b/rackspace/php-opencloud/lib/OpenCloud/Common/Resource/ReadOnlyResource.php new file mode 100644 index 000000000..132136a2e --- /dev/null +++ b/rackspace/php-opencloud/lib/OpenCloud/Common/Resource/ReadOnlyResource.php @@ -0,0 +1,42 @@ +noCreate(); + } + + public function update($params = array()) + { + return $this->noUpdate(); + } + + public function delete() + { + return $this->noDelete(); + } +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Common/Service/AbstractService.php b/rackspace/php-opencloud/lib/OpenCloud/Common/Service/AbstractService.php index 06b6c9d22..f528480d1 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Common/Service/AbstractService.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Common/Service/AbstractService.php @@ -1,19 +1,26 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Common\Service; use Guzzle\Http\ClientInterface; use OpenCloud\Common\Base; -use OpenCloud\Common\Exceptions; use OpenCloud\Common\Collection\PaginatedIterator; +use OpenCloud\Common\Exceptions; /** * This class defines a cloud service; a relationship between a specific OpenStack @@ -26,7 +33,7 @@ abstract class AbstractService extends Base implements ServiceInterface { /** - * @var OpenCloud\Common\Http\Client The client which interacts with the API. + * @var \OpenCloud\Common\Http\Client The client which interacts with the API. */ protected $client; @@ -54,7 +61,7 @@ public function setClient(ClientInterface $client) } /** - * @return OpenCloud\Common\Http\Client + * @return \OpenCloud\Common\Http\Client */ public function getClient() { @@ -96,6 +103,7 @@ public function getResources() protected function getCurrentNamespace() { $namespace = get_class($this); + return substr($namespace, 0, strrpos($namespace, '\\')); } @@ -127,8 +135,8 @@ protected function resolveResourceClass($resourceName) * Factory method for instantiating resource objects. * * @param string $resourceName - * @param mixed $info (default: null) - * @param mixed $parent The parent object + * @param mixed $info (default: null) + * @param mixed $parent The parent object * @return object */ public function resource($resourceName, $info = null, $parent = null) @@ -136,6 +144,7 @@ public function resource($resourceName, $info = null, $parent = null) $className = $this->resolveResourceClass($resourceName); $resource = new $className($this); + if ($parent) { $resource->setParent($parent); } @@ -151,11 +160,12 @@ public function resource($resourceName, $info = null, $parent = null) * @param string $resourceName * @param string|null $url * @param string|null $parent - * @return OpenCloud\Common\Collection + * @return PaginatedIterator */ public function resourceList($resourceName, $url = null, $parent = null) { $className = $this->resolveResourceClass($resourceName); + return $this->collection($className, $url, $parent); } @@ -198,4 +208,4 @@ public function getNamespaces() ? $this->namespaces : array(); } -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Common/Service/Catalog.php b/rackspace/php-opencloud/lib/OpenCloud/Common/Service/Catalog.php index cb3cb74de..f3fb8d6b6 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Common/Service/Catalog.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Common/Service/Catalog.php @@ -1,10 +1,18 @@ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Common\Service; @@ -46,7 +54,7 @@ public static function factory($config) print_r($config, true) )); } - + return $catalog; } @@ -57,4 +65,4 @@ public function getItems() { return $this->items; } -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Common/Service/CatalogItem.php b/rackspace/php-opencloud/lib/OpenCloud/Common/Service/CatalogItem.php index 59235afd2..d6bf956ee 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Common/Service/CatalogItem.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Common/Service/CatalogItem.php @@ -1,11 +1,18 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Common\Service; @@ -43,7 +50,7 @@ public static function factory($object) $item->setName($object->name) ->setType($object->type) ->setEndpoints($object->endpoints); - + return $item; } @@ -54,6 +61,7 @@ public static function factory($object) public function setName($name) { $this->name = $name; + return $this; } @@ -83,6 +91,7 @@ public function hasName($string) public function setType($type) { $this->type = $type; + return $this; } @@ -110,6 +119,7 @@ public function hasType($string) public function setEndpoints(array $endpoints) { $this->endpoints = $endpoints; + return $this; } @@ -124,14 +134,18 @@ public function getEndpoints() /** * Using a standard data object, extract its endpoint. * - * @param $region + * @param string $region + * @param bool $isRegionless + * * @return mixed * @throws \OpenCloud\Common\Exceptions\EndpointError */ - public function getEndpointFromRegion($region) + public function getEndpointFromRegion($region, $isRegionless = false) { foreach ($this->endpoints as $endpoint) { - if (!isset($endpoint->region) || $endpoint->region == $region) { + // Return the endpoint if it is regionless OR if the endpoint's + // region matches the $region supplied by the caller. + if ($isRegionless || !isset($endpoint->region) || $endpoint->region == $region) { return $endpoint; } } @@ -142,5 +156,4 @@ public function getEndpointFromRegion($region) $region )); } - -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Common/Service/CatalogService.php b/rackspace/php-opencloud/lib/OpenCloud/Common/Service/CatalogService.php index b28f94be6..0e26405c5 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Common/Service/CatalogService.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Common/Service/CatalogService.php @@ -1,17 +1,35 @@ hasLogger()) { + $this->setLogger($client->getLogger()); + } + $this->setClient($client); - $this->name = $name ?: static::DEFAULT_NAME; + $this->name = $name ? : static::DEFAULT_NAME; + $this->region = $region; - if ($this->regionless !== true && !($this->region = $region)) { + $this->region = $region; + if ($this->regionless !== true && !$this->region) { throw new Exceptions\ServiceException(sprintf( 'The %s service must have a region set. You can either pass in a region string as an argument param, or' . ' set a default region for your user account by executing User::setDefaultRegion and ::update().', @@ -67,8 +91,8 @@ public function __construct(ClientInterface $client, $type = null, $name = null, )); } - $this->type = $type ?: static::DEFAULT_TYPE; - $this->urlType = $urlType ?: static::DEFAULT_URL_TYPE; + $this->type = $type ? : static::DEFAULT_TYPE; + $this->urlType = $urlType ? : static::DEFAULT_URL_TYPE; $this->setEndpoint($this->findEndpoint()); $this->client->setBaseUrl($this->getBaseUrl()); @@ -157,6 +181,7 @@ public function url($path = null, array $query = array()) public function getExtensions() { $ext = $this->getMetaUrl('extensions'); + return (is_object($ext) && isset($ext->extensions)) ? $ext->extensions : array(); } @@ -168,6 +193,7 @@ public function getExtensions() public function limits() { $limits = $this->getMetaUrl('limits'); + return (is_object($limits)) ? $limits->limits : array(); } @@ -189,7 +215,8 @@ private function findEndpoint() // Search each service to find The One foreach ($catalog->getItems() as $service) { if ($service->hasType($this->type) && $service->hasName($this->name)) { - return Endpoint::factory($service->getEndpointFromRegion($this->region)); + $endpoint = $service->getEndpointFromRegion($this->region, $this->regionless); + return Endpoint::factory($endpoint, static::SUPPORTED_VERSION, $this->getClient()); } } @@ -209,7 +236,7 @@ private function findEndpoint() * URL and retrieves the resource. * * @param string $resource The resource requested; should NOT have slashes - * at the beginning or end + * at the beginning or end * @return \stdClass object */ private function getMetaUrl($resource) @@ -218,6 +245,7 @@ private function getMetaUrl($resource) $url->addPath($resource); try { $response = $this->getClient()->get($url)->send(); + return Formatter::decode($response); } catch (BadResponseException $e) { // @codeCoverageIgnoreStart @@ -248,5 +276,4 @@ public function getBaseUrl() return $url; } - -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Common/Service/Endpoint.php b/rackspace/php-opencloud/lib/OpenCloud/Common/Service/Endpoint.php index 11cbe3a79..2388dc258 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Common/Service/Endpoint.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Common/Service/Endpoint.php @@ -1,15 +1,26 @@ + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Common\Service; use Guzzle\Http\Url; +use OpenCloud\OpenStack; +use OpenCloud\Common\Http\Message\Formatter; +use OpenCloud\Common\Exceptions\UnsupportedVersionError; /** * An endpoint serves as a location which receives and emits API interactions. It will therefore also host @@ -36,17 +47,19 @@ class Endpoint /** * @param $object + * @param string $supportedServiceVersion Service version supported by the SDK + * @param OpenCloud\OpenStack $client OpenStack client * @return Endpoint */ - public static function factory($object) + public static function factory($object, $supportedServiceVersion, OpenStack $client) { $endpoint = new self(); - + if (isset($object->publicURL)) { - $endpoint->setPublicUrl($object->publicURL); + $endpoint->setPublicUrl($endpoint->getVersionedUrl($object->publicURL, $supportedServiceVersion, $client)); } if (isset($object->internalURL)) { - $endpoint->setPrivateUrl($object->internalURL); + $endpoint->setPrivateUrl($endpoint->getVersionedUrl($object->internalURL, $supportedServiceVersion, $client)); } if (isset($object->region)) { $endpoint->setRegion($object->region); @@ -59,9 +72,10 @@ public static function factory($object) * @param $publicUrl * @return $this */ - public function setPublicUrl($publicUrl) + public function setPublicUrl(Url $publicUrl) { - $this->publicUrl = Url::factory($publicUrl); + $this->publicUrl = $publicUrl; + return $this; } @@ -77,9 +91,10 @@ public function getPublicUrl() * @param $privateUrl * @return $this */ - public function setPrivateUrl($privateUrl) + public function setPrivateUrl(Url $privateUrl) { - $this->privateUrl = Url::factory($privateUrl); + $this->privateUrl = $privateUrl; + return $this; } @@ -98,6 +113,7 @@ public function getPrivateUrl() public function setRegion($region) { $this->region = $region; + return $this; } @@ -108,5 +124,54 @@ public function getRegion() { return $this->region; } - -} \ No newline at end of file + + /** + * Returns the endpoint URL with a version in it + * + * @param string $url Endpoint URL + * @param string $supportedServiceVersion Service version supported by the SDK + * @param OpenCloud\OpenStack $client OpenStack client + * @return Guzzle/Http/Url Endpoint URL with version in it + */ + private function getVersionedUrl($url, $supportedServiceVersion, OpenStack $client) + { + $versionRegex = '/\/[vV][0-9][0-9\.]*/'; + if (1 === preg_match($versionRegex, $url)) { + // URL has version in it; use it as-is + return Url::factory($url); + } + + // If there is no version in $url but no $supportedServiceVersion + // is specified, just return $url as-is but log a warning + if (is_null($supportedServiceVersion)) { + $client->getLogger()->warning('Service version supported by SDK not specified. Using versionless service URL as-is, without negotiating version.'); + return Url::factory($url); + } + + // Make GET request to URL + $response = Formatter::decode($client->get($url)->send()); + + // Attempt to parse response and determine URL for given $version + if (!isset($response->versions) || !is_array($response->versions)) { + throw new UnsupportedVersionError('Could not negotiate version with service.'); + } + + foreach ($response->versions as $version) { + if (($version->status == 'CURRENT' || $version->status == 'SUPPORTED') + && $version->id == $supportedServiceVersion) { + foreach ($version->links as $link) { + if ($link->rel == 'self') { + return Url::factory($link->href); + } + } + } + } + + // If we've reached this point, we could not find a versioned + // URL in the response; throw an error + throw new UnsupportedVersionError(sprintf( + 'SDK supports version %s which is not currently provided by service.', + $supportedServiceVersion + )); + } +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Common/Service/NovaService.php b/rackspace/php-opencloud/lib/OpenCloud/Common/Service/NovaService.php index 0b73bd9bd..e03508ad4 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Common/Service/NovaService.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Common/Service/NovaService.php @@ -1,17 +1,22 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Common\Service; -use OpenCloud\OpenStack; -use OpenCloud\Common\Lang; use OpenCloud\Compute\Resource\Flavor; /** @@ -19,48 +24,46 @@ */ abstract class NovaService extends CatalogService { - - /** - * Returns a flavor from the service - * + /** + * Returns a flavor from the service + * * @param string|null $id * @return Flavor */ public function flavor($id = null) - { - return new Flavor($this, $id); - } + { + return new Flavor($this, $id); + } - /** - * Returns a list of Flavor objects + /** + * Returns a list of Flavor objects * - * @param boolean $details Returns full details or not. - * @param array $filter Array for creating queries - * @return Collection - */ - public function flavorList($details = true, array $filter = array()) - { + * @param boolean $details Returns full details or not. + * @param array $filter Array for creating queries + * @return Collection + */ + public function flavorList($details = true, array $filter = array()) + { $path = Flavor::resourceName(); - + if ($details === true) { $path .= '/detail'; } return $this->collection('OpenCloud\Compute\Resource\Flavor', $this->getUrl($path, $filter)); - } + } - /** - * Loads the available namespaces from the /extensions resource - */ - protected function loadNamespaces() + /** + * Loads the available namespaces from the /extensions resource + */ + protected function loadNamespaces() { - foreach ($this->getExtensions() as $object) { - $this->namespaces[] = $object->alias; - } + foreach ($this->getExtensions() as $object) { + $this->namespaces[] = $object->alias; + } if (!empty($this->additionalNamespaces)) { $this->namespaces += $this->additionalNamespaces; } - } - + } } diff --git a/rackspace/php-opencloud/lib/OpenCloud/Common/Service/ServiceBuilder.php b/rackspace/php-opencloud/lib/OpenCloud/Common/Service/ServiceBuilder.php index 8226afa02..99747e900 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Common/Service/ServiceBuilder.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Common/Service/ServiceBuilder.php @@ -1,10 +1,18 @@ + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Common\Service; @@ -17,19 +25,19 @@ */ class ServiceBuilder { - /** * Simple factory method for creating services. - * - * @param Client $client The HTTP client object - * @param string $class The class name of the service - * @param array $options The options. + * + * @param Client $client The HTTP client object + * @param string $class The class name of the service + * @param array $options The options. * @return \OpenCloud\Common\Service\ServiceInterface * @throws ServiceException */ public static function factory(ClientInterface $client, $class, array $options = array()) { $name = isset($options['name']) ? $options['name'] : null; + $type = isset($options['type']) ? $options['type'] : null; $urlType = isset($options['urlType']) ? $options['urlType'] : null; if (isset($options['region'])) { @@ -40,7 +48,6 @@ public static function factory(ClientInterface $client, $class, array $options = $region = null; } - return new $class($client, null, $name, $region, $urlType); + return new $class($client, $type, $name, $region, $urlType); } - -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Common/Service/ServiceInterface.php b/rackspace/php-opencloud/lib/OpenCloud/Common/Service/ServiceInterface.php index 89e264edc..d10749e73 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Common/Service/ServiceInterface.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Common/Service/ServiceInterface.php @@ -1,4 +1,19 @@ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Compute\Constants; -class ImageState +class ImageState { - const ACTIVE = 'ACTIVE'; + const ACTIVE = 'ACTIVE'; const DELETED = 'DELETED'; - const ERROR = 'ERROR'; - const SAVING = 'SAVING'; + const ERROR = 'ERROR'; + const SAVING = 'SAVING'; const UNKNOWN = 'UNKNOWN'; -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Compute/Constants/Network.php b/rackspace/php-opencloud/lib/OpenCloud/Compute/Constants/Network.php index 7f5a4cc7c..0c39b2de8 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Compute/Constants/Network.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Compute/Constants/Network.php @@ -1,26 +1,31 @@ - * @author Glen Campbell + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Compute\Constants; - -class Network +class Network { /** * UUID of Rackspace public network */ - const RAX_PUBLIC = '00000000-0000-0000-0000-000000000000'; + const RAX_PUBLIC = '00000000-0000-0000-0000-000000000000'; /** * UUID of Rackspace private network (i.e. ServiceNet) */ const RAX_PRIVATE = '11111111-1111-1111-1111-111111111111'; - -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Compute/Constants/ServerState.php b/rackspace/php-opencloud/lib/OpenCloud/Compute/Constants/ServerState.php index d3083521b..05a6e9f0b 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Compute/Constants/ServerState.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Compute/Constants/ServerState.php @@ -1,16 +1,23 @@ + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Compute\Constants; - -class ServerState +class ServerState { /** * The server is active and ready to use. @@ -91,7 +98,6 @@ class ServerState */ const VERIFY_RESIZE = 'VERIFY_RESIZE'; - const REBOOT_STATE_HARD = 'HARD'; const REBOOT_STATE_SOFT = 'SOFT'; -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Compute/Exception/KeyPairException.php b/rackspace/php-opencloud/lib/OpenCloud/Compute/Exception/KeyPairException.php index c199cf61c..479c03bf7 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Compute/Exception/KeyPairException.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Compute/Exception/KeyPairException.php @@ -1,13 +1,22 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Compute\Exception; -class KeyPairException extends \Exception {} \ No newline at end of file +class KeyPairException extends \Exception +{ +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Compute/Resource/Flavor.php b/rackspace/php-opencloud/lib/OpenCloud/Compute/Resource/Flavor.php index cf5560254..b21a1fe97 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Compute/Resource/Flavor.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Compute/Resource/Flavor.php @@ -1,11 +1,18 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Compute\Resource; @@ -13,12 +20,11 @@ use OpenCloud\Common\PersistentObject; /** - * A resource configuration for a server. Each flavor is a unique combination + * A resource configuration for a server. Each flavor is a unique combination * of disk, memory, vCPUs, and network bandwidth. */ -class Flavor extends PersistentObject +class Flavor extends PersistentObject { - public $status; public $updated; public $vcpus; @@ -33,19 +39,18 @@ class Flavor extends PersistentObject protected static $json_name = 'flavor'; protected static $url_resource = 'flavors'; - public function create($params = array()) - { - return $this->noCreate(); + public function create($params = array()) + { + return $this->noCreate(); } - public function update($params = array()) - { - return $this->noUpdate(); + public function update($params = array()) + { + return $this->noUpdate(); } - public function delete() + public function delete() { return $this->noDelete(); } - } diff --git a/rackspace/php-opencloud/lib/OpenCloud/Compute/Resource/Image.php b/rackspace/php-opencloud/lib/OpenCloud/Compute/Resource/Image.php index 27a59872d..e1afa4679 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Compute/Resource/Image.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Compute/Resource/Image.php @@ -1,30 +1,38 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Compute\Resource; use OpenCloud\Common\PersistentObject; +use OpenCloud\Image\Resource\ImageInterface; /** - * A collection of files for a specific operating system (OS) that you use to - * create or rebuild a server. Rackspace provides pre-built images. You can also - * create custom images from servers that you have launched. Custom images can + * A collection of files for a specific operating system (OS) that you use to + * create or rebuild a server. Rackspace provides pre-built images. You can also + * create custom images from servers that you have launched. Custom images can * be used for data backups or as "gold" images for additional servers. * - * @note In the future, this may be abstracted to access Glance (the OpenStack - * image store) directly, but it is currently not available to Rackspace + * @note In the future, this may be abstracted to access Glance (the OpenStack + * image store) directly, but it is currently not available to Rackspace * customers, so we're using the /images resource on the servers API endpoint. */ -class Image extends PersistentObject -{ +class Image extends PersistentObject implements ImageInterface +{ public $status; public $updated; public $links; @@ -40,20 +48,23 @@ class Image extends PersistentObject protected static $json_name = 'image'; protected static $url_resource = 'images'; - /** - * {@inheritDoc} - */ - public function create($params = array()) - { - return $this->noCreate(); + public function setId($id) + { + $this->id = $id; + } + + public function getId() + { + return $this->id; } - /** - * {@inheritDoc} - */ - public function update($params = array()) - { - return $this->noUpdate(); + public function create($params = array()) + { + return $this->noCreate(); } + public function update($params = array()) + { + return $this->noUpdate(); + } } diff --git a/rackspace/php-opencloud/lib/OpenCloud/Compute/Resource/KeyPair.php b/rackspace/php-opencloud/lib/OpenCloud/Compute/Resource/KeyPair.php index c3de348d6..a17357d79 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Compute/Resource/KeyPair.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Compute/Resource/KeyPair.php @@ -1,18 +1,25 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Compute\Resource; +use OpenCloud\Common\Exceptions\InvalidArgumentError; use OpenCloud\Common\PersistentObject; use OpenCloud\Compute\Exception\KeyPairException; -use OpenCloud\Common\Exceptions\InvalidArgumentError; class KeyPair extends PersistentObject { @@ -21,17 +28,17 @@ class KeyPair extends PersistentObject private $privateKey; private $publicKey; private $userId; - + public $aliases = array( 'private_key' => 'privateKey', 'public_key' => 'publicKey', 'user_id' => 'userId' ); - + protected static $url_resource = 'os-keypairs'; - protected static $json_name = 'keypair'; + protected static $json_name = 'keypair'; protected static $json_collection_element = 'keypair'; - + public function setName($name) { if (preg_match('#[^\w\d\s-_]#', $name) || strlen($name) > 255) { @@ -43,14 +50,15 @@ public function setName($name) )); } $this->name = $name; + return $this; } - + public function getName() { return $this->name; } - + /** * {@inheritDoc} */ @@ -60,20 +68,22 @@ public function createJson() 'name' => $this->getName() ); if (null !== ($key = $this->getPublicKey())) { - $object->public_key = $key; + $object->public_key = $key; } + return (object) array('keypair' => $object); } public function create($params = array()) { $this->setPublicKey(null); + return parent::create($params); } - + /** * Upload an existing public key to a new keypair. - * + * * @param array $options * @return type * @throws KeyPairException @@ -93,6 +103,7 @@ public function upload(array $options = array()) 'In order to upload a keypair, the public key must be set.' ); } + return parent::create(); } @@ -105,5 +116,4 @@ public function primaryKeyField() { return 'name'; } - -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Compute/Resource/Network.php b/rackspace/php-opencloud/lib/OpenCloud/Compute/Resource/Network.php index 25b90f7a3..8b041df97 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Compute/Resource/Network.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Compute/Resource/Network.php @@ -1,32 +1,38 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Compute\Resource; use Guzzle\Http\Url; -use OpenCloud\Common\PersistentObject; -use OpenCloud\Common\Lang; use OpenCloud\Common\Exceptions; -use OpenCloud\Compute\Service; +use OpenCloud\Common\Resource\PersistentResource; use OpenCloud\Compute\Constants\Network as NetworkConst; +use OpenCloud\Compute\Service; +use OpenCloud\Networking\Resource\NetworkInterface; /** * The Network class represents a single virtual network */ -class Network extends PersistentObject +class Network extends PersistentResource implements NetworkInterface { - public $id; public $label; public $cidr; - + protected static $json_name = 'network'; protected static $url_resource = 'os-networksv2'; protected static $openStackResourcePath = 'os-networks'; @@ -38,12 +44,12 @@ class Network extends PersistentObject * networks. These are not really networks, but they show up in lists. * * @param \OpenCloud\Compute\Service $service The compute service associated with - * the network - * @param string|null $id The ID of the network (this handles the pseudo-networks - * Network::RAX_PUBLIC and Network::RAX_PRIVATE + * the network + * @param string|null $id The ID of the network (this handles the pseudo-networks + * Network::RAX_PUBLIC and Network::RAX_PRIVATE * @return Network */ - public function __construct(Service $service, $id = null) + public function __construct(Service $service, $id = null) { $this->id = $id; @@ -59,7 +65,7 @@ public function __construct(Service $service, $id = null) default: return parent::__construct($service, $id); } - + return; } @@ -68,7 +74,7 @@ public function __construct(Service $service, $id = null) * * @throws Exceptions\NetworkUpdateError always */ - public function update($params = array()) + public function update($params = array()) { throw new Exceptions\NetworkUpdateError('Isolated networks cannot be updated'); } @@ -80,7 +86,7 @@ public function update($params = array()) * @return \OpenCloud\HttpResponse * @throws NetworkDeleteError if HTTP status is not Success */ - public function delete() + public function delete() { switch ($this->id) { case NetworkConst::RAX_PUBLIC: @@ -90,14 +96,14 @@ public function delete() return parent::delete(); } } - + /** * returns the visible name (label) of the network * * @api * @return string */ - public function name() + public function name() { return $this->label; } @@ -105,13 +111,13 @@ public function name() /** * Creates the JSON object for the Create() method */ - protected function createJson() + protected function createJson() { return (object) array( 'network' => (object) array( - 'cidr' => $this->cidr, - 'label' => $this->label - ) + 'cidr' => $this->cidr, + 'label' => $this->label + ) ); } @@ -122,7 +128,6 @@ protected function createJson() public function getUrl($path = null, array $query = array()) { if (!$url = $this->findLink('self')) { - $url = $this->getParent()->getUrl($this->getResourcePath()); if (null !== ($primaryKey = $this->getProperty($this->primaryKeyField()))) { @@ -151,4 +156,8 @@ public function getResourcePath() } } -} \ No newline at end of file + public function getId() + { + return $this->id; + } +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Compute/Resource/Server.php b/rackspace/php-opencloud/lib/OpenCloud/Compute/Resource/Server.php index a3c7aed35..9038c291d 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Compute/Resource/Server.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Compute/Resource/Server.php @@ -1,153 +1,207 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Compute\Resource; -use OpenCloud\Common\PersistentObject; +use OpenCloud\Common\Resource\NovaResource; +use OpenCloud\DNS\Resource\HasPtrRecordsInterface; +use OpenCloud\Image\Resource\ImageInterface; +use OpenCloud\Networking\Resource\NetworkInterface; +use OpenCloud\Networking\Resource\SecurityGroup; +use OpenCloud\Networking\Resource\Port; use OpenCloud\Volume\Resource\Volume; use OpenCloud\Common\Exceptions; +use OpenCloud\Common\Http\Message\Formatter; use OpenCloud\Common\Lang; -use OpenCloud\Compute\Service; use OpenCloud\Compute\Constants\ServerState; -use OpenCloud\Common\Http\Message\Formatter; +use OpenCloud\Compute\Service; /** * A virtual machine (VM) instance in the Cloud Servers environment. * - * @note This implementation supports extension attributes OS-DCF:diskConfig, - * RAX-SERVER:bandwidth, rax-bandwidth:bandwith. + * @note This implementation supports extension attributes OS-DCF:diskConfig, + * RAX-SERVER:bandwidth, rax-bandwidth:bandwidth. */ -class Server extends PersistentObject +class Server extends NovaResource implements HasPtrRecordsInterface { /** * The server status. {@see \OpenCloud\Compute\Constants\ServerState} for supported types. - * - * @var string + * + * @var string */ public $status; - + /** * @var string The time stamp for the last update. */ public $updated; - + /** - * The compute provisioning algorithm has an anti-affinity property that - * attempts to spread customer VMs across hosts. Under certain situations, - * VMs from the same customer might be placed on the same host. $hostId - * represents the host your server runs on and can be used to determine this + * The compute provisioning algorithm has an anti-affinity property that + * attempts to spread customer VMs across hosts. Under certain situations, + * VMs from the same customer might be placed on the same host. $hostId + * represents the host your server runs on and can be used to determine this * scenario if it is relevant to your application. - * - * @var string + * + * @var string */ public $hostId; - + /** * @var type Public and private IP addresses for this server. */ public $addresses; - + /** * @var array Server links. */ public $links; - + /** * The Image for this server. - * + * * @link http://docs.rackspace.com/servers/api/v2/cs-devguide/content/List_Images-d1e4435.html - * @var type + * @var ImageInterface */ public $image; - + + /** + * The bootable volume for this server. + * + * @var Volume + */ + public $volume; + + /** + * Whether to delete the bootable volume when the server is terminated (deleted). + * @var boolean + */ + public $volumeDeleteOnTermination; + /** * The Flavor for this server. - * + * * @link http://docs.rackspace.com/servers/api/v2/cs-devguide/content/List_Flavors-d1e4188.html - * @var type + * @var type */ public $flavor; - + /** - * @var type + * @var type */ public $networks = array(); - + + /** + * Security groups for this server. An array of either the names or SecurityGroup objects. + * @var (string|SecurityGroup)[] + */ + public $security_groups = array(); + /** * @var string The server ID. */ public $id; - + /** * @var string The user ID. */ public $user_id; - + /** * @var string The server name. */ public $name; - + /** * @var string The time stamp for the creation date. */ public $created; - + /** * @var string The tenant ID. */ public $tenant_id; - - /** + + /** * @var string The public IP version 4 access address. */ public $accessIPv4; - + /** * @var string The public IP version 6 access address. */ public $accessIPv6; - + /** * The build completion progress, as a percentage. Value is from 0 to 100. - - * @var int + * @var int */ public $progress; - + /** * @var string The root password (only populated on server creation). */ public $adminPass; - + /** * @var mixed Metadata key and value pairs. */ public $metadata; + /** + * @link http://docs.rackspace.com/servers/api/v2/cs-devguide/content/ext_status.html + * @var string Virtual machine status. + */ + public $extendedStatus; + + /** + * @link http://docs.rackspace.com/servers/api/v2/cs-devguide/content/ext_status.html + * @var string Status indicating a running task + */ + public $taskStatus; + + /** + * @link http://docs.rackspace.com/servers/api/v2/cs-devguide/content/ext_status.html + * @var int Power status of the VM + */ + public $powerStatus; + + /** + * @link http://developer.openstack.org/api-ref-compute-v2-ext.html#ext-os-ext-az + * @var string Availability zone of the VM + */ + public $availabilityZone; + protected static $json_name = 'server'; protected static $url_resource = 'servers'; /** @var string|object Keypair or string representation of keypair name */ public $keypair; - + /** * @var array Uploaded file attachments */ private $personality = array(); - + /** * @var type Image reference (for create) */ private $imageRef; - + /** * @var type Flavor reference (for create) */ @@ -159,16 +213,26 @@ class Server extends PersistentObject */ public $user_data; + /** + * {@inheritDoc} + */ + protected $aliases = array( + 'OS-EXT-STS:vm_state' => 'extendedStatus', + 'OS-EXT-STS:task_state' => 'taskStatus', + 'OS-EXT-STS:power_state' => 'powerStatus', + 'OS-EXT-AZ:availability_zone' => 'availabilityZone' + ); + /** * Creates a new Server object and associates it with a Compute service * * @param mixed $info - * * If NULL, an empty Server object is created - * * If an object, then a Server object is created from the data in the + * * If NULL, an empty Server object is created + * * If an object, then a Server object is created from the data in the * object - * * If a string, then it's treated as a Server ID and retrieved from the + * * If a string, then it's treated as a Server ID and retrieved from the * service - * The normal use case for SDK clients is to treat it as either NULL or an + * The normal use case for SDK clients is to treat it as either NULL or an * ID. The object value parameter is a special case used to construct * a Server object from a ServerList element to avoid a secondary * call to the Service. @@ -217,9 +281,17 @@ public function ip($type = null) */ public function create($params = array()) { - $this->id = null; + $this->id = null; $this->status = null; - + + if (isset($params['imageId'])) { + $this->imageRef = $params['imageId']; + } + + if (isset($params['flavorId'])) { + $this->flavorRef = $params['flavorId']; + } + return parent::create($params); } @@ -228,30 +300,30 @@ public function create($params = array()) * * @api * @param array $params - an associative array of key/value pairs of - * attributes to set on the new server + * attributes to set on the new server */ public function rebuild($params = array()) { - if (!isset($params['adminPass'])) { - throw new Exceptions\RebuildError( - Lang::Translate('adminPass required when rebuilding server') + if (!isset($params['adminPass'])) { + throw new Exceptions\RebuildError( + Lang::Translate('adminPass required when rebuilding server') ); } - + if (!isset($params['image'])) { - throw new Exceptions\RebuildError( - Lang::Translate('image required when rebuilding server') + throw new Exceptions\RebuildError( + Lang::Translate('image required when rebuilding server') ); } - + $object = (object) array( 'rebuild' => (object) array( - 'imageRef' => $params['image']->id(), - 'adminPass' => $params['adminPass'], - 'name' => (array_key_exists('name', $params) ? $params['name'] : $this->name) - ) + 'imageRef' => $params['image']->id(), + 'adminPass' => $params['adminPass'], + 'name' => (array_key_exists('name', $params) ? $params['name'] : $this->name) + ) ); - + return $this->action($object); } @@ -280,8 +352,8 @@ public function reboot($type = null) * Creates a new image from a server * * @api - * @param string $name The name of the new image - * @param array $metadata Optional metadata to be stored on the image + * @param string $name The name of the new image + * @param array $metadata Optional metadata to be stored on the image * @return boolean|Image New Image instance on success; FALSE on failure * @throws Exceptions\ImageError */ @@ -289,18 +361,18 @@ public function createImage($name, $metadata = array()) { if (empty($name)) { throw new Exceptions\ImageError( - Lang::translate('Image name is required to create an image') + Lang::translate('Image name is required to create an image') ); } // construct a createImage object for jsonization $object = (object) array('createImage' => (object) array( - 'name' => $name, - 'metadata' => (object) $metadata - )); + 'name' => $name, + 'metadata' => (object) $metadata + )); $response = $this->action($object); - + if (!$response || !($location = $response->getHeader('Location'))) { return false; } @@ -313,10 +385,10 @@ public function createImage($name, $metadata = array()) * * @api * @param mixed $retention - false (default) indicates you want to - * retrieve the image schedule. $retention <= 0 indicates you - * want to delete the current schedule. $retention > 0 indicates - * you want to schedule image backups and you would like to - * retain $retention backups. + * retrieve the image schedule. $retention <= 0 indicates you + * want to delete the current schedule. $retention > 0 indicates + * you want to schedule image backups and you would like to + * retain $retention backups. * @return mixed an object or FALSE on error * @throws Exceptions\ServerImageScheduleError if an error is encountered */ @@ -324,16 +396,16 @@ public function imageSchedule($retention = false) { $url = $this->getUrl('rax-si-image-schedule'); - if ($retention === false) { + if ($retention === false) { // Get current retention $request = $this->getClient()->get($url); - } elseif ($retention <= 0) { + } elseif ($retention <= 0) { // Delete image schedule $request = $this->getClient()->delete($url); - } else { + } else { // Set image schedule - $object = (object) array('image_schedule' => - (object) array('retention' => $retention) + $object = (object) array('image_schedule' => + (object) array('retention' => $retention) ); $body = json_encode($object); $request = $this->getClient()->post($url, self::getJsonHeader(), $body); @@ -348,7 +420,7 @@ public function imageSchedule($retention = false) * Initiates the resize of a server * * @api - * @param Flavor $flavorRef a Flavor object indicating the new server size + * @param Flavor $flavorRef a Flavor object indicating the new server size * @return boolean TRUE on success; FALSE on failure */ public function resize(Flavor $flavorRef) @@ -357,6 +429,7 @@ public function resize(Flavor $flavorRef) $object = (object) array( 'resize' => (object) array('flavorRef' => $flavorRef->id) ); + return $this->action($object); } @@ -371,6 +444,7 @@ public function resizeConfirm() $object = (object) array('confirmResize' => null); $response = $this->action($object); $this->refresh($this->id); + return $response; } @@ -383,6 +457,7 @@ public function resizeConfirm() public function resizeRevert() { $object = (object) array('revertResize' => null); + return $this->action($object); } @@ -390,7 +465,7 @@ public function resizeRevert() * Sets the root password on the server * * @api - * @param string $newPassword The new root password for the server + * @param string $newPassword The new root password for the server * @return boolean TRUE on success; FALSE on failure */ public function setPassword($newPassword) @@ -398,6 +473,7 @@ public function setPassword($newPassword) $object = (object) array( 'changePassword' => (object) array('adminPass' => $newPassword) ); + return $this->action($object); } @@ -424,7 +500,7 @@ public function rescue() $response = $this->action($data); $body = Formatter::decode($response); - + return (isset($body->adminPass)) ? $body->adminPass : false; } @@ -446,6 +522,7 @@ public function unrescue() } $object = (object) array('unrescue' => null); + return $this->action($object); } @@ -471,17 +548,17 @@ public function metadata($key = null) * * @api * @param string $network - if supplied, then only the IP(s) for the - * specified network are returned. Otherwise, all IPs are returned. + * specified network are returned. Otherwise, all IPs are returned. * @return object * @throws Exceptions\ServerIpsError */ public function ips($network = null) { - $url = Lang::noslash($this->Url('ips/'.$network)); + $url = Lang::noslash($this->Url('ips/' . $network)); - $response = $this->getClient()->get($url)->send(); + $response = $this->getClient()->get($url)->send(); $body = Formatter::decode($response); - + return (isset($body->addresses)) ? $body->addresses : ((isset($body->network)) ? $body->network : (object) array()); } @@ -494,18 +571,19 @@ public function ips($network = null) * * @api * @param OpenCloud\Volume\Resource\Volume $volume The volume to attach. If - * "auto" is specified (the default), then the first available - * device is used to mount the volume (for example, if the primary - * disk is on `/dev/xvhda`, then the new volume would be attached - * to `/dev/xvhdb`). - * @param string $device the device to which to attach it + * "auto" is specified (the default), then the first available + * device is used to mount the volume (for example, if the primary + * disk is on `/dev/xvhda`, then the new volume would be attached + * to `/dev/xvhdb`). + * @param string $device the device to which to attach it */ public function attachVolume(Volume $volume, $device = 'auto') { $this->checkExtension('os-volumes'); + return $this->volumeAttachment()->create(array( - 'volumeId' => $volume->id, - 'device' => ($device == 'auto' ? null : $device) + 'volumeId' => $volume->id, + 'device' => ($device == 'auto' ? null : $device) )); } @@ -514,12 +592,12 @@ public function attachVolume(Volume $volume, $device = 'auto') * * Requires the os-volumes extension. This is a synonym for * `VolumeAttachment::delete()` - * @param OpenCloud\Volume\Resource\Volume $volume The volume to remove */ public function detachVolume(Volume $volume) { $this->checkExtension('os-volumes'); + return $this->volumeAttachment($volume->id)->delete(); } @@ -531,12 +609,12 @@ public function volumeAttachment($id = null) { $resource = new VolumeAttachment($this->getService()); $resource->setParent($this)->populate($id); + return $resource; } /** * Returns a Collection of VolumeAttachment objects - * @return Collection */ public function volumeAttachmentList() @@ -551,7 +629,7 @@ public function volumeAttachmentList() * * @api * @param string $path The path where the file will be stored on the - * target server (up to 255 characters) + * target server (up to 255 characters) * @param string $data the file contents (max size set by provider) * @return void */ @@ -560,18 +638,18 @@ public function addFile($path, $data) $this->personality[$path] = base64_encode($data); } - /** - * Returns a console connection + /** + * Returns a console connection * Note: Where is this documented? - * + * * @codeCoverageIgnore - */ - public function console($type = 'novnc') + */ + public function console($type = 'novnc') { $action = (strpos('spice', $type) !== false) ? 'os-getSPICEConsole' : 'os-getVNCConsole'; $object = (object) array($action => (object) array('type' => $type)); - - $response = $this->action($object); + + $response = $this->action($object); $body = Formatter::decode($response); return (isset($body->console)) ? $body->console : false; @@ -581,55 +659,83 @@ protected function createJson() { // Convert some values $this->metadata->sdk = $this->getService()->getClient()->getUserAgent(); - - if (!empty($this->image) && $this->image instanceof Image) { - $this->imageRef = $this->image->id; + + if ($this->image instanceof ImageInterface) { + $this->imageRef = $this->image->getId(); } - if (!empty($this->flavor) && $this->flavor instanceof Flavor) { + if ($this->flavor instanceof Flavor) { $this->flavorRef = $this->flavor->id; } - + // Base object $server = (object) array( - 'name' => $this->name, - 'imageRef' => $this->imageRef, - 'flavorRef' => $this->flavorRef + 'name' => $this->name, + 'imageRef' => $this->imageRef, + 'flavorRef' => $this->flavorRef ); if ($this->metadata->count()) { $server->metadata = $this->metadata->toArray(); } - - // Networks - if (is_array($this->networks) && count($this->networks)) { + // Boot from volume + if ($this->volume instanceof Volume) { + $this->checkExtension('os-block-device-mapping-v2-boot'); + + $server->block_device_mapping_v2 = array(); + $server->block_device_mapping_v2[] = (object) array( + 'source_type' => 'volume', + 'destination_type' => 'volume', + 'uuid' => $this->volume->id, + 'boot_index' => 0, + 'delete_on_termination' => (boolean) $this->volumeDeleteOnTermination + ); + } + + // Networks + if (is_array($this->networks) && count($this->networks)) { $server->networks = array(); - foreach ($this->networks as $network) { - if (!$network instanceof Network) { - throw new Exceptions\InvalidParameterError(sprintf( - 'When creating a server, the "networks" key must be an ' . - 'array of OpenCloud\Compute\Network objects with valid ' . - 'IDs; variable passed in was a [%s]', - gettype($network) - )); - } - if (empty($network->id)) { - $this->getLogger()->warning('When creating a server, the ' - . 'network objects passed in must have an ID' - ); - continue; + foreach ($this->networks as $network) { + if ($network instanceof NetworkInterface) { + $server->networks[] = (object) array('uuid' => $network->getId()); + } elseif ($network instanceof Port) { + $server->networks[] = (object) array('port' => $network->getId()); + } else { + throw new Exceptions\InvalidParameterError(sprintf( + 'When creating a server, the "networks" key must be an ' . + 'array of objects which implement either OpenCloud\Networking\Resource\NetworkInterface ' . + 'or OpenCloud\Networking\Resource\Port. The variable you passed in was a [%s]', + gettype($network) + )); + } + } + } + + // Security groups + if (is_array($this->security_groups) && count($this->security_groups)) { + $server->security_groups = array(); + + foreach ($this->security_groups as $security_group) { + if ($security_group instanceof SecurityGroup) { + $securityGroupName = $security_group->name(); + } elseif (is_string($security_group)) { + $securityGroupName = $security_group; + } else { + throw new Exceptions\InvalidParameterError(sprintf( + 'When creating a server, the "security_groups" key must be an ' . + 'array of strings or objects of type OpenCloud\Networking\Resource\SecurityGroup;' . + 'variable passed in was a [%s]', + gettype($security_group) + )); } - // Stock networks array - $server->networks[] = (object) array('uuid' => $network->id); - } - } + $server->security_groups[] = (object) array('name' => $securityGroupName); + } + } // Personality files if (!empty($this->personality)) { - $server->personality = array(); - foreach ($this->personality as $path => $data) { // Stock personality array $server->personality[] = (object) array( @@ -638,7 +744,7 @@ protected function createJson() ); } } - + // Keypairs if (!empty($this->keypair)) { if (is_string($this->keypair)) { @@ -652,7 +758,13 @@ protected function createJson() // Cloud-init executable if (!empty($this->user_data)) { - $server->user_data = $this->user_data; + $server->user_data = $this->user_data; + } + + // Availability zone + if (!empty($this->availabilityZone)) { + $this->checkExtension('OS-EXT-AZ'); + $server->availability_zone = $this->availabilityZone; } return (object) array('server' => $server); @@ -663,4 +775,100 @@ protected function updateJson($params = array()) return (object) array('server' => (object) $params); } + /** + * Suspend a server + * + * A suspend request suspend an instance, its VM state is stored on disk, all memory is written + * to disk, and the virtual machine is stopped. Suspending an instance is similar to placing a + * device in hibernation; memory and vCPUs become available to create other instances. + * + * @api + * @return \Guzzle\Http\Message\Response + */ + public function suspend() + { + // The suspend action is only available when the os-admin-actions extension is installed. + $this->checkExtension('os-admin-actions'); + + $object = (object) array('suspend' => 'none'); + + return $this->action($object); + } + + /** + * Resume a server + * + * A resume request resumes a suspended instance, its VM state was stored on disk, all memory was written + * to disk, and the virtual machine was stopped. Resuming a suspended instance is similar to resuming a + * device from hibernation. + * + * @api + * @return \Guzzle\Http\Message\Response + */ + public function resume() + { + // The resume action is only available when the os-admin-actions extension is installed. + $this->checkExtension('os-admin-actions'); + + $object = (object) array('resume' => 'none'); + + return $this->action($object); + } + + /** + * Get server diagnostics + * + * Gets basic usage data for a specified server. + * + * @api + * @return object + */ + public function diagnostics() + { + // The diagnostics is only available when the os-server-diagnostics extension is installed. + $this->checkExtension('os-server-diagnostics'); + + $url = $this->getUrl('diagnostics'); + + $response = $this->getClient()->get($url)->send(); + $body = Formatter::decode($response); + + return $body ?: (object) array(); + } + + /** + * Start a server + * + * Starts a stopped server and changes its status to ACTIVE. + * + * @api + * @return \Guzzle\Http\Message\Response + */ + public function start() + { + // The start action is only available when the os-server-start-stop extension is installed. + $this->checkExtension('os-server-start-stop'); + + $object = (object) array('os-start' => null); + + return $this->action($object); + } + + /** + * Stop a server + * + * Stops a running server and changes its status to STOPPED. + * + * @api + * @return \Guzzle\Http\Message\Response + */ + public function stop() + { + // The stop action is only available when the os-server-start-stop extension is installed. + $this->checkExtension('os-server-start-stop'); + + $object = (object) array('os-stop' => null); + + return $this->action($object); + } } diff --git a/rackspace/php-opencloud/lib/OpenCloud/Compute/Resource/ServerMetadata.php b/rackspace/php-opencloud/lib/OpenCloud/Compute/Resource/ServerMetadata.php index ea389362f..203e2d63a 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Compute/Resource/ServerMetadata.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Compute/Resource/ServerMetadata.php @@ -1,23 +1,30 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Compute\Resource; -use OpenCloud\Common\Lang; -use OpenCloud\Common\Metadata; -use OpenCloud\Common\Exceptions; use Guzzle\Http\Url; +use OpenCloud\Common\Exceptions; use OpenCloud\Common\Http\Message\Formatter; +use OpenCloud\Common\Lang; +use OpenCloud\Common\Metadata; /** - * This class handles specialized metadata for OpenStack Server objects (metadata + * This class handles specialized metadata for OpenStack Server objects (metadata * items can be managed individually or in aggregate). * * Server metadata is a weird beast in that it has resource representations @@ -27,14 +34,14 @@ class ServerMetadata extends Metadata { private $parent; - protected $key; // the metadata item (if supplied) - private $url; // the URL of this particular metadata item or block + protected $key; // the metadata item (if supplied) + private $url; // the URL of this particular metadata item or block /** - * Contructs a Metadata object associated with a Server or Image object + * Constructs a Metadata object associated with a Server or Image object * * @param object $parent either a Server or an Image object - * @param string $key the (optional) key for the metadata item + * @param string $key the (optional) key for the metadata item * @throws MetadataError */ public function __construct(Server $parent, $key = null) @@ -44,7 +51,6 @@ public function __construct(Server $parent, $key = null) // set the URL according to whether or not we have a key if ($this->getParent()->getId()) { - $this->url = $this->getParent()->url('metadata'); $this->key = $key; @@ -73,6 +79,7 @@ public function getParent() public function setParent($parent) { $this->parent = $parent; + return $this; } @@ -153,7 +160,7 @@ public function __set($key, $value) // otherwise, just set it; parent::__set($key, $value); } - + /** * Builds a metadata JSON string * @@ -172,16 +179,12 @@ private function getMetadataJson() if ($name = $this->key) { $object->meta->$name = $this->$name; } else { - $object->metadata = new \stdClass(); - foreach ($this->keylist() as $key) { - $object->metadata->$key = (string) $this->$key; - } + $object->metadata = $this->keylist(); } $json = json_encode($object); $this->checkJsonError(); - + return $json; } - } diff --git a/rackspace/php-opencloud/lib/OpenCloud/Compute/Resource/VolumeAttachment.php b/rackspace/php-opencloud/lib/OpenCloud/Compute/Resource/VolumeAttachment.php index 66aaf57b4..07892942c 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Compute/Resource/VolumeAttachment.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Compute/Resource/VolumeAttachment.php @@ -1,25 +1,31 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Compute\Resource; -use OpenCloud\Common\Lang; use OpenCloud\Common\Exceptions; +use OpenCloud\Common\Lang; use OpenCloud\Common\PersistentObject; /** * The VolumeAttachment class represents a volume that is attached to a server. */ -class VolumeAttachment extends PersistentObject +class VolumeAttachment extends PersistentObject { - public $id; public $device; public $serverId; @@ -35,7 +41,7 @@ class VolumeAttachment extends PersistentObject * * @throws OpenCloud\UpdateError always */ - public function update($params = array()) + public function update($params = array()) { throw new Exceptions\UpdateError(Lang::translate('Updates are not permitted')); } @@ -48,9 +54,9 @@ public function update($params = array()) * @api * @return string */ - public function name() + public function name() { - return sprintf('Attachment [%s]', $this->volumeId ?: 'N/A'); + return sprintf('Attachment [%s]', $this->volumeId ? : 'N/A'); } /** @@ -58,11 +64,11 @@ public function name() * * @return stdClass */ - protected function createJson() + protected function createJson() { $object = new \stdClass; - - foreach($this->createKeys as $key) { + + foreach ($this->createKeys as $key) { if (isset($this->$key)) { $object->$key = $this->$key; } @@ -72,5 +78,4 @@ protected function createJson() $this->jsonName() => $object ); } - } diff --git a/rackspace/php-opencloud/lib/OpenCloud/Compute/Service.php b/rackspace/php-opencloud/lib/OpenCloud/Compute/Service.php index c15a3e465..37d66a036 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Compute/Service.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Compute/Service.php @@ -1,19 +1,25 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Compute; +use OpenCloud\Common\Exceptions; use OpenCloud\Common\Http\Client; -use OpenCloud\Common\Lang; use OpenCloud\Common\Service\NovaService; -use OpenCloud\Common\Exceptions; /** * The Compute class represents the OpenStack Nova service. @@ -55,9 +61,10 @@ public function __construct(Client $client, $type = null, $name = null, $region { parent::__construct($client, $type, $name, $region, $urlType); - if (strpos($this->getUrl()->getPath(), '/v1') !== false) { + // @see https://github.com/rackspace/php-opencloud/issues/353 + if (strpos($this->getUrl()->getPath(), '/v1.0/') !== false) { throw new Exceptions\UnsupportedVersionError(sprintf( - Lang::translate('Sorry; API version /v1 is not supported [%s]'), + 'Sorry; API version /v1.0 is not supported [%s]', $this->getUrl() )); } @@ -74,9 +81,9 @@ public function __construct(Client $client, $type = null, $name = null, $region * * @api * @param string $id - if specified, the server with the ID is retrieved - * @returns Compute\Server object + * @return Resource\Server object */ - public function server($id = null) + public function server($id = null) { return new Resource\Server($this, $id); } @@ -90,16 +97,17 @@ public function server($id = null) * * @api * @param boolean $details - if TRUE, full server details are returned; if - * FALSE, just the minimal set of info is listed. Defaults to TRUE; - * you might set this to FALSE to improve performance at the risk of - * not having all the information you need. - * @param array $filter - a set of key/value pairs that is passed to the - * servers list for filtering - * @returns \OpenCloud\Common\Collection + * FALSE, just the minimal set of info is listed. Defaults to TRUE; + * you might set this to FALSE to improve performance at the risk of + * not having all the information you need. + * @param array $filter - a set of key/value pairs that is passed to the + * servers list for filtering + * @return \OpenCloud\Common\Collection\PaginatedIterator */ - public function serverList($details = true, array $filter = array()) + public function serverList($details = true, array $filter = array()) { $url = $this->getUrl(Resource\Server::resourceName() . (($details) ? '/detail' : ''), $filter); + return $this->collection('OpenCloud\Compute\Resource\Server', $url); } @@ -110,7 +118,7 @@ public function serverList($details = true, array $filter = array()) * @param string $id the network ID * @return Resource\Network */ - public function network($id = null) + public function network($id = null) { return new Resource\Network($this, $id); } @@ -120,9 +128,9 @@ public function network($id = null) * * @api * @param array $filter array of filter key/value pairs - * @return \OpenCloud\Common\Collection + * @return \OpenCloud\Common\Collection\PaginatedIterator */ - public function networkList($filter = array()) + public function networkList($filter = array()) { return $this->collection('OpenCloud\Compute\Resource\Network'); } @@ -137,7 +145,7 @@ public function networkList($filter = array()) * @param string $id - if supplied, returns the image with the specified ID. * @return Resource\Image object */ - public function image($id = null) + public function image($id = null) { return new Resource\Image($this, $id); } @@ -150,16 +158,16 @@ public function image($id = null) * * @api * @param boolean $details - if TRUE (the default), returns complete image - * details. Set to FALSE to improve performance, but only return a - * minimal set of data - * @param array $filter - key/value pairs to pass to the images resource. - * The actual values available here are determined by the OpenStack - * code and any extensions installed by your cloud provider; - * see http://docs.rackspace.com/servers/api/v2/cs-devguide/content/List_Images-d1e4435.html - * for current filters available. + * details. Set to FALSE to improve performance, but only return a + * minimal set of data + * @param array $filter - key/value pairs to pass to the images resource. + * The actual values available here are determined by the OpenStack + * code and any extensions installed by your cloud provider; + * see http://docs.rackspace.com/servers/api/v2/cs-devguide/content/List_Images-d1e4435.html + * for current filters available. * @return \OpenCloud\Common\Collection */ - public function imageList($details = true, array $filter = array()) + public function imageList($details = true, array $filter = array()) { $url = clone $this->getUrl(); $url->addPath('images'); @@ -175,15 +183,13 @@ public function imageList($details = true, array $filter = array()) return $this->resourceList('Image', $url); } - public function keypair($data = null) { return $this->resource('KeyPair', $data); } - + public function listKeypairs() { return $this->resourceList('KeyPair', null, $this); } - } diff --git a/rackspace/php-opencloud/lib/OpenCloud/DNS/Collection/DnsIterator.php b/rackspace/php-opencloud/lib/OpenCloud/DNS/Collection/DnsIterator.php index 50dd725e3..f10da3088 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/DNS/Collection/DnsIterator.php +++ b/rackspace/php-opencloud/lib/OpenCloud/DNS/Collection/DnsIterator.php @@ -1,4 +1,19 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\DNS\Resource; -use OpenCloud\Common\PersistentObject; -use OpenCloud\Common\Lang; use OpenCloud\Common\Exceptions; use OpenCloud\Common\Http\Message\Formatter; +use OpenCloud\Common\Lang; +use OpenCloud\Common\Resource\PersistentResource; -/** - * The DnsObject class is an extension of the PersistentObject class that - * permits the asynchronous responses used by Cloud DNS - */ -abstract class Object extends PersistentObject +abstract class AbstractResource extends PersistentResource { - - /** - * Create() returns an asynchronous response - * - * @param array $params array of key/value pairs - * @return AsyncResponse - */ - public function create($params = array()) + public function create($params = array()) { $body = Formatter::decode(parent::create($params)); + return new AsyncResponse($this->getService(), $body); } - /** - * Update() returns an asynchronous response - * - * @param array $params array of key/value pairs - * @return AsyncResponse - */ - public function update($params = array()) + public function update($params = array()) { $response = parent::update($params); $body = Formatter::decode($response); + return new AsyncResponse($this->getService(), $body); } - /** - * Delete() returns an asynchronous response - * - * @param array $params array of key/value pairs - * @return AsyncResponse - */ - public function delete() + public function delete() { $body = Formatter::decode(parent::delete()); + return new AsyncResponse($this->getService(), $body); } - /** - * creates the JSON for create - * - * @return stdClass - */ - protected function createJson() + protected function createJson() { if (!$this->getCreateKeys()) { throw new Exceptions\CreateError( @@ -79,18 +61,14 @@ protected function createJson() ); } - /** - * creates the JSON for update - * - * @return stdClass - */ - protected function updateJson($params = array()) + protected function updateJson($params = array()) { if (!$this->getUpdateKeys()) { throw new Exceptions\UpdateError( Lang::translate('Missing [updateKeys]') ); } + return $this->getJson($this->getUpdateKeys()); } @@ -100,35 +78,35 @@ protected function updateJson($params = array()) * @param array $keys list of items to include * @return stdClass */ - private function getJson($keys) + private function getJson($keys) { $object = new \stdClass; - foreach($keys as $item) { + foreach ($keys as $item) { if (!empty($this->$item)) { $object->$item = $this->$item; } } + return $object; } - + /** * Retrieve the keys which are required when the object is created. - * + * * @return array|false */ public function getCreateKeys() { return (!empty($this->createKeys)) ? $this->createKeys : false; } - + /** * Retrieve the keys which are required when the object is updated. - * + * * @return array|false */ public function getUpdateKeys() { return (!empty($this->updateKeys)) ? $this->updateKeys : false; } - } diff --git a/rackspace/php-opencloud/lib/OpenCloud/DNS/Resource/AsyncResponse.php b/rackspace/php-opencloud/lib/OpenCloud/DNS/Resource/AsyncResponse.php index 71421d05b..952aa1642 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/DNS/Resource/AsyncResponse.php +++ b/rackspace/php-opencloud/lib/OpenCloud/DNS/Resource/AsyncResponse.php @@ -1,16 +1,24 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\DNS\Resource; use Guzzle\Http\Url; +use OpenCloud\Common\Constants\State; use OpenCloud\Common\PersistentObject; use OpenCloud\Common\Service\ServiceInterface; @@ -20,6 +28,7 @@ */ class AsyncResponse extends PersistentObject { + const DEFAULT_INTERVAL = 2; public $jobId; public $callbackUrl; @@ -38,7 +47,7 @@ class AsyncResponse extends PersistentObject * string * * @param \OpenCloud\Service $service the calling service - * @param string $json the json response from the initial request + * @param string $json the json response from the initial request */ public function __construct(ServiceInterface $service, $object = null) { @@ -95,4 +104,33 @@ public function primaryKeyField() return 'jobId'; } + public function waitFor($state = null, $timeout = null, $callback = null, $interval = null) + { + $state = $state ?: 'COMPLETED'; + $timeout = $timeout ?: State::DEFAULT_TIMEOUT; + $interval = $interval ?: self::DEFAULT_INTERVAL; + + $jobUrl = Url::factory($this->callbackUrl); + $jobUrl->setQuery(array('showDetails' => 'true')); + + $continue = true; + $startTime = time(); + $states = array('ERROR', $state); + + while ($continue) { + $body = $this->getClient()->get($jobUrl)->send()->json(); + + if ($callback) { + call_user_func($callback, $body); + } + + if (in_array($body['status'], $states) || (time() - $startTime) > $timeout) { + $continue = false; + } + + sleep($interval); + } + + return isset($body['response']) ? $body['response'] : false; + } } diff --git a/rackspace/php-opencloud/lib/OpenCloud/DNS/Resource/Domain.php b/rackspace/php-opencloud/lib/OpenCloud/DNS/Resource/Domain.php index fe04ebd8d..a103677f5 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/DNS/Resource/Domain.php +++ b/rackspace/php-opencloud/lib/OpenCloud/DNS/Resource/Domain.php @@ -1,11 +1,18 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\DNS\Resource; @@ -18,9 +25,8 @@ * Note that the `Subdomain` class is defined in this same file because of * mutual dependencies. */ -class Domain extends Object +class Domain extends AbstractResource { - public $id; public $accountId; public $ttl; @@ -30,7 +36,7 @@ class Domain extends Object public $name; public $comment; - protected static $json_name = FALSE; + protected static $json_name = false; protected static $json_collection_name = 'domains'; protected static $url_resource = 'domains'; @@ -62,6 +68,7 @@ public function record($info = null) { $resource = new Record($this->getService()); $resource->setParent($this)->populate($info); + return $resource; } @@ -69,7 +76,7 @@ public function record($info = null) * returns a Collection of Record objects * * @param array $filter query-string parameters - * @return \OpenCloud\Collection + * @return OpenCloud\DNS\Collection\DnsIterator */ public function recordList($filter = array()) { @@ -88,6 +95,7 @@ public function subdomain($info = array()) { $resource = new Subdomain($this->getService()); $resource->setParent($this)->populate($info); + return $resource; } @@ -98,7 +106,8 @@ public function subdomain($info = array()) * the current domain. * * @param array $filter key/value pairs for query string parameters - * return \OpenCloud\Collection + * return \OpenCloud\Collection + * @return OpenCloud\DNS\Collection\DnsIterator */ public function subdomainList($filter = array()) { @@ -119,6 +128,7 @@ public function subdomainList($filter = array()) public function addRecord(Record $record) { $this->records[] = $record; + return count($this->records); } @@ -132,6 +142,7 @@ public function addRecord(Record $record) public function addSubdomain(Subdomain $subdomain) { $this->subdomains[] = $subdomain; + return count($this->subdomains); } @@ -142,9 +153,14 @@ public function addSubdomain(Subdomain $subdomain) * @return DNS\Changes */ public function changes($since = null) - { - $url = $this->url('changes', isset($since) ? array('since' => $since) : array()); - + { + $url = clone $this->getUrl(); + $url->addPath('changes'); + + if ($since) { + $url->setQuery(array('since' => $since)); + } + $response = $this->getService() ->getClient() ->get($url) @@ -160,45 +176,47 @@ public function changes($since = null) */ public function export() { - return $this->getService()->asyncRequest($this->url('export')); + return $this->getService()->asyncRequest($this->getUrl('export')); } /** * clones the domain to the specified target domain * - * @param string $newdomain the new domain to create from this domain - * @param boolean $sub to clone subdomains as well - * @param boolean $comments Replace occurrences of the reference domain - * name with the new domain name in comments - * @param boolean $email Replace occurrences of the reference domain - * name with the new domain name in email addresses on the cloned - * (new) domain. - * @param boolean $records Replace occurrences of the reference domain - * name with the new domain name in data fields (of records) on the - * cloned (new) domain. Does not affect NS records. + * @param string $newdomain the new domain to create from this domain + * @param boolean $sub to clone subdomains as well + * @param boolean $comments Replace occurrences of the reference domain + * name with the new domain name in comments + * @param boolean $email Replace occurrences of the reference domain + * name with the new domain name in email addresses on the cloned + * (new) domain. + * @param boolean $records Replace occurrences of the reference domain + * name with the new domain name in data fields (of records) on the + * cloned (new) domain. Does not affect NS records. * @return AsyncResponse */ public function cloneDomain( - $newdomain, - $sub = true, + $newDomainName, + $subdomains = true, $comments = true, $email = true, $records = true ) { - $url = $this->url('clone', array( - 'cloneName' => $newdomain, - 'cloneSubdomains' => $sub, + $url = $this->getUrl(); + $url->addPath('clone'); + $url->setQuery(array( + 'cloneName' => $newDomainName, + 'cloneSubdomains' => $subdomains, 'modifyComment' => $comments, 'modifyEmailAddress' => $email, 'modifyRecordData' => $records )); + return $this->getService()->asyncRequest($url, 'POST'); } /** * handles creation of multiple records at Create() * - * @api * @return \stdClass */ protected function createJson() @@ -207,12 +225,11 @@ protected function createJson() // add records, if any if (count($this->records)) { - $recordsObject = (object) array('records' => array()); foreach ($this->records as $record) { $recordObject = new \stdClass; - foreach($record->getCreateKeys() as $key) { + foreach ($record->getCreateKeys() as $key) { if (isset($record->$key)) { $recordObject->$key = $record->$key; } @@ -224,12 +241,11 @@ protected function createJson() // add subdomains, if any if (count($this->subdomains)) { - $subdomainsObject = (object) array('domains' => array()); - foreach($this->subdomains as $subdomain) { + foreach ($this->subdomains as $subdomain) { $subdomainObject = new \stdClass; - foreach($subdomain->getCreateKeys() as $key) { + foreach ($subdomain->getCreateKeys() as $key) { if (isset($subdomain->$key)) { $subdomainObject->$key = $subdomain->$key; } @@ -241,5 +257,4 @@ protected function createJson() return $object; } - } diff --git a/rackspace/php-opencloud/lib/OpenCloud/DNS/Resource/HasPtrRecordsInterface.php b/rackspace/php-opencloud/lib/OpenCloud/DNS/Resource/HasPtrRecordsInterface.php new file mode 100644 index 000000000..d21a4d3ae --- /dev/null +++ b/rackspace/php-opencloud/lib/OpenCloud/DNS/Resource/HasPtrRecordsInterface.php @@ -0,0 +1,12 @@ + - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\DNS\Resource; -use OpenCloud\Common\Lang; -use OpenCloud\Common\Exceptions; use OpenCloud\Common\Http\Message\Formatter; /** @@ -20,9 +25,9 @@ * The PtrRecord object is nearly identical with the Record object. However, * the PtrRecord is a child of the service, and not a child of a Domain. */ -class PtrRecord extends Record +class PtrRecord extends Record { - + /** @var HasPtrRecordsInterface The device which this record refers to */ public $server; protected static $json_name = false; @@ -32,75 +37,91 @@ class PtrRecord extends Record private $link_rel; private $link_href; + public function __construct($service, $info = null) + { + parent::__construct($service, $info); + + $this->type = 'PTR'; + } + /** - * constructur ensures that the record type is PTR + * Used to internally populate this object with the appropriate type checks + * + * @param array $params + * @throws \InvalidArgumentException If no parent device set */ - public function __construct($parent, $info = null) + protected function populateRecord(array $params = array()) { - $this->type = 'PTR'; - parent::__construct($parent, $info); - if ($this->type != 'PTR') { - throw new Exceptions\RecordTypeError(sprintf( - Lang::translate('Invalid record type [%s], must be PTR'), - $this->type - )); + if (!isset($params['parent'])) { + throw new \InvalidArgumentException('You must set a `parent` device'); } + + $this->setDeviceParent($params['parent']); + unset($params['parent']); + + parent::populate($params); } /** - * DNS PTR Create() method requires a server + * Set the parent device * - * Generally called as `Create(array('server'=>$server))` + * @param HasPtrRecordsInterface $parent */ - public function create($params = array()) + public function setDeviceParent(HasPtrRecordsInterface $parent) { - $this->populate($params); - $this->link_rel = $this->server->getService()->name(); - $this->link_href = $this->server->url(); - return parent::create(); + $this->server = $parent; } /** - * DNS PTR Update() method requires a server + * @return HasPtrRecordsInterface */ - public function update($params = array()) + public function getDeviceParent() { - $this->populate($params); - $this->link_rel = $this->server->getService()->Name(); - $this->link_href = $this->server->Url(); + return $this->server; + } + + public function create($params = array()) + { + $this->populateRecord($params); + + $this->link_rel = $this->getDeviceParent()->getService()->getName(); + $this->link_href = (string) $this->getDeviceParent()->getUrl(); + + return parent::create(); + } + + public function update($params = array()) + { + $this->populateRecord($params); + + $this->link_rel = $this->getDeviceParent()->getService()->getName(); + $this->link_href = (string) $this->getDeviceParent()->getUrl(); + return parent::update(); } - /** - * DNS PTR Delete() method requires a server - * - * Note that delete will remove ALL PTR records associated with the device - * unless you pass in the parameter ip={ip address} - * - */ - public function delete() + public function delete() { - $this->link_rel = $this->server->getService()->Name(); - $this->link_href = $this->server->Url(); - + $this->link_rel = $this->getDeviceParent()->getService()->Name(); + $this->link_href = (string) $this->getDeviceParent()->getUrl(); + $params = array('href' => $this->link_href); if (!empty($this->data)) { $params['ip'] = $this->data; } - + $url = clone $this->getUrl(); - $url->addPath('rdns') + $url->addPath('..') + ->normalizePath() ->addPath($this->link_rel) ->setQuery($params); $response = $this->getClient()->delete($url)->send(); + return new AsyncResponse($this->getService(), Formatter::decode($response)); } - /** - * Specialized JSON for DNS PTR creates and updates - */ - protected function createJson() + protected function createJson() { return (object) array( 'recordsList' => parent::createJson(), @@ -111,15 +132,13 @@ protected function createJson() ); } - /** - * The Update() JSON requires a record ID - */ - protected function updateJson($params = array()) + protected function updateJson($params = array()) { $this->populate($params); + $object = $this->createJson(); $object->recordsList->records[0]->id = $this->id; + return $object; } - } diff --git a/rackspace/php-opencloud/lib/OpenCloud/DNS/Resource/Record.php b/rackspace/php-opencloud/lib/OpenCloud/DNS/Resource/Record.php index 1ce788563..4eb91c065 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/DNS/Resource/Record.php +++ b/rackspace/php-opencloud/lib/OpenCloud/DNS/Resource/Record.php @@ -1,25 +1,29 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\DNS\Resource; -use OpenCloud\DNS\Service; - /** * The Record class represents a single domain record * * This is also used for PTR records. */ -class Record extends Object +class Record extends AbstractResource { - public $ttl; public $updated; public $created; @@ -35,7 +39,7 @@ class Record extends Object protected static $url_resource = 'records'; protected $parent; - + protected $updateKeys = array( 'name', 'ttl', @@ -43,7 +47,7 @@ class Record extends Object 'priority', 'comment' ); - + protected $createKeys = array( 'type', 'name', @@ -52,5 +56,4 @@ class Record extends Object 'priority', 'comment' ); - } diff --git a/rackspace/php-opencloud/lib/OpenCloud/DNS/Resource/Subdomain.php b/rackspace/php-opencloud/lib/OpenCloud/DNS/Resource/Subdomain.php index 9dd9630b9..1d5019e5d 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/DNS/Resource/Subdomain.php +++ b/rackspace/php-opencloud/lib/OpenCloud/DNS/Resource/Subdomain.php @@ -1,11 +1,18 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\DNS\Resource; @@ -16,11 +23,9 @@ * different than a top-level domain, and the parent is a domain instead of * the DNS service itself. */ -class Subdomain extends Domain +class Subdomain extends Domain { - protected static $json_name = false; protected static $json_collection_name = 'domains'; protected static $url_resource = 'subdomains'; - } diff --git a/rackspace/php-opencloud/lib/OpenCloud/DNS/Service.php b/rackspace/php-opencloud/lib/OpenCloud/DNS/Service.php index 48f008849..651353e14 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/DNS/Service.php +++ b/rackspace/php-opencloud/lib/OpenCloud/DNS/Service.php @@ -1,20 +1,29 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\DNS; -use OpenCloud\Common\Service\CatalogService; -use OpenCloud\OpenStack; -use OpenCloud\Compute\Resource\Server; use OpenCloud\Common\Http\Message\Formatter; +use OpenCloud\Common\Service\CatalogService; +use OpenCloud\Common\Exceptions\DomainNotFoundException; use OpenCloud\DNS\Collection\DnsIterator; +use OpenCloud\DNS\Resource\AsyncResponse; +use OpenCloud\DNS\Resource\Domain; +use OpenCloud\DNS\Resource\HasPtrRecordsInterface; /** * DNS Service. @@ -31,34 +40,52 @@ public function collection($class, $url = null, $parent = null, $data = null) $options = $this->makeResourceIteratorOptions($this->resolveResourceClass($class)); $options['baseUrl'] = $url; - $parent = $parent ?: $this; + $parent = $parent ? : $this; return DnsIterator::factory($parent, $options, $data); } /** - * returns a DNS::Domain object + * Returns a domain * - * @api * @param mixed $info either the ID, an object, or array of parameters * @return Resource\Domain */ public function domain($info = null) { - return new Resource\Domain($this, $info); + return $this->resource('Domain', $info); + } + + /** + * Returns a domain, given a domain name + * + * @param string $domainName Domain name + * @return Domain the domain object + */ + public function domainByName($domainName) + { + $domainList = $this->domainList(array("name" => $domainName)); + + if (count($domainList) != 1) { + throw new DomainNotFoundException(); + } + + return $this->resource('Domain', $domainList[0]); } + /** - * returns a Collection of DNS::Domain objects + * Returns a collection of domains * - * @api * @param array $filter key/value pairs to use as query strings - * @return \OpenCloud\Common\Collection + * @return OpenCloud\DNS\Collection\DnsIterator */ public function domainList($filter = array()) { - $url = $this->getUrl(Resource\Domain::resourceName(), $filter); - return $this->collection('OpenCloud\DNS\Resource\Domain', $url); + $url = $this->getUrl(Domain::resourceName()); + $url->setQuery($filter); + + return $this->resourceList('Domain', $url); } /** @@ -69,24 +96,24 @@ public function domainList($filter = array()) */ public function ptrRecord($info = null) { - return new Resource\PtrRecord($this, $info); + return $this->resource('PtrRecord', $info); } /** * returns a Collection of PTR records for a given Server * * @param \OpenCloud\Compute\Resource\Server $server the server for which to - * retrieve the PTR records - * @return \OpenCloud\Common\Collection + * retrieve the PTR records + * @return OpenCloud\DNS\Collection\DnsIterator */ - public function ptrRecordList(Server $server) + public function ptrRecordList(HasPtrRecordsInterface $parent) { $url = $this->getUrl() ->addPath('rdns') - ->addPath($server->getService()->name()) - ->setQuery(array('href' => $server->url())); + ->addPath($parent->getService()->getName()) + ->setQuery(array('href' => (string) $parent->getUrl())); - return $this->collection('OpenCloud\DNS\Resource\PtrRecord', $url); + return $this->resourceList('PtrRecord', $url); } /** @@ -97,34 +124,35 @@ public function ptrRecordList(Server $server) * an `AsyncResponse` object. This object can then be used to poll * for the status or to retrieve the final data as needed. * - * @param string $url the URL of the request - * @param string $method the HTTP method to use - * @param array $headers key/value pairs for headers to include - * @param string $body the body of the request (for PUT and POST) + * @param string $url the URL of the request + * @param string $method the HTTP method to use + * @param array $headers key/value pairs for headers to include + * @param string $body the body of the request (for PUT and POST) * @return Resource\AsyncResponse */ public function asyncRequest($url, $method = 'GET', $headers = array(), $body = null) { $response = $this->getClient()->createRequest($method, $url, $headers, $body)->send(); - return new Resource\AsyncResponse($this, Formatter::decode($response)); + + return new AsyncResponse($this, Formatter::decode($response)); } /** - * imports domain records + * Imports domain records * * Note that this function is called from the service (DNS) level, and * not (as you might suspect) from the Domain object. Because the function * return an AsyncResponse, the domain object will not actually exist * until some point after the import has occurred. * - * @api * @param string $data the BIND_9 formatted data to import * @return Resource\AsyncResponse */ public function import($data) { - // determine the URL - $url = $this->url('domains/import'); + $url = clone $this->getUrl(); + $url->addPath('domains'); + $url->addPath('import'); $object = (object) array( 'domains' => array( @@ -138,11 +166,8 @@ public function import($data) // encode it $json = json_encode($object); - // debug it - $this->getLogger()->info('Importing [{json}]', array('json' => $json)); - // perform the request - return $this->asyncRequest($url, 'POST', array(), $json); + return $this->asyncRequest($url, 'POST', self::getJsonHeader(), $json); } /** @@ -150,12 +175,16 @@ public function import($data) */ public function limits($type = null) { - $url = $this->url('limits') . ($type ? "/$type" : ''); + $url = $this->getUrl('limits'); + + if ($type) { + $url->addPath($type); + } $response = $this->getClient()->get($url)->send(); $body = Formatter::decode($response); - return ($body) ? $body : $body->limits; + return isset($body->limits) ? $body->limits : $body; } /** @@ -167,7 +196,39 @@ public function limitTypes() { $response = $this->getClient()->get($this->getUrl('limits/types'))->send(); $body = Formatter::decode($response); + return $body->limitTypes; } + /** + * List asynchronous responses' statuses. + * @see http://docs.rackspace.com/cdns/api/v1.0/cdns-devguide/content/viewing_status_all_asynch_jobs.html + * + * @param array $query Any query parameters. Optional. + * @return OpenCloud\DNS\Collection\DnsIterator + */ + public function listAsyncJobs(array $query = array()) + { + $url = clone $this->getUrl(); + $url->addPath('status'); + $url->setQuery($query); + + return DnsIterator::factory($this, array( + 'baseUrl' => $url, + 'resourceClass' => 'AsyncResponse', + 'key.collection' => 'asyncResponses' + )); + } + + public function getAsyncJob($jobId, $showDetails = true) + { + $url = clone $this->getUrl(); + $url->addPath('status'); + $url->addPath((string) $jobId); + $url->setQuery(array('showDetails' => ($showDetails) ? 'true' : 'false')); + + $response = $this->getClient()->get($url)->send(); + + return new AsyncResponse($this, Formatter::decode($response)); + } } diff --git a/rackspace/php-opencloud/lib/OpenCloud/Database/Resource/Backup.php b/rackspace/php-opencloud/lib/OpenCloud/Database/Resource/Backup.php new file mode 100644 index 000000000..97b015398 --- /dev/null +++ b/rackspace/php-opencloud/lib/OpenCloud/Database/Resource/Backup.php @@ -0,0 +1,92 @@ + 'Instance' + ); + + protected $aliases = array( + 'instance_id' => 'instanceId' + ); + + public function __construct(Service $service, $info = null) + { + $this->instance = new \stdClass; + return parent::__construct($service, $info); + } + + /** + * Returns the JSON object for creating the backup + */ + protected function createJson() + { + if (!isset($this->instanceId)) { + throw new Exceptions\BackupInstanceError( + Lang::translate('The `instanceId` attribute is required and must be a string') + ); + } + + if (!isset($this->name)) { + throw new Exceptions\BackupNameError( + Lang::translate('Backup name is required') + ); + } + + $out = [ + 'backup' => [ + 'name' => $this->name, + 'instance' => $this->instanceId + ] + ]; + + if (isset($this->description)) { + $out['backup']['description'] = $this->description; + } + return (object) $out; + } +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Database/Resource/Configuration.php b/rackspace/php-opencloud/lib/OpenCloud/Database/Resource/Configuration.php new file mode 100644 index 000000000..cbbbcf4f2 --- /dev/null +++ b/rackspace/php-opencloud/lib/OpenCloud/Database/Resource/Configuration.php @@ -0,0 +1,84 @@ +updateJson($params)); + $this->checkJsonError(); + + return $this->getClient()->patch($this->url(), self::getJsonHeader(), $json)->send(); + } + + /** + * Generates the JSON string for update() + * + * @return \@stdClass + */ + protected function updateJson($params = array()) + { + return (object) array( + self::$json_name => $params + ); + } + + /** + * Returns a Collection of all instances using this configuration + * + * @return OpenCloud\Common\Collection\PaginatedIterator + */ + public function instanceList() + { + return $this->getService()->resourceList('Instance', $this->getUrl('instances')); + } +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Database/Resource/Database.php b/rackspace/php-opencloud/lib/OpenCloud/Database/Resource/Database.php index 2a4a4b2bc..e643e7aaf 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Database/Resource/Database.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Database/Resource/Database.php @@ -1,63 +1,54 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Database\Resource; -use OpenCloud\Common\PersistentObject; use OpenCloud\Common\Exceptions; use OpenCloud\Common\Lang; +use OpenCloud\Common\Resource\PersistentResource; /** * This class represents a Database in the Rackspace "Red Dwarf" * database-as-a-service product. */ -class Database extends PersistentObject +class Database extends PersistentResource { + /** @var string */ + public $name; + protected static $json_collection_name = 'databases'; protected static $url_resource = 'databases'; - - public $name; - /** - * Creates a new database object - * - * Unlike other objects (Servers, DataObjects, etc.), passing a database - * name to the constructor does *not* pull information from the database. - * For example, if you pass an ID to the `Server()` constructor, it will - * attempt to retrieve the information on that server from the service, - * and will return an error if it is not found. However, the Cloud - * Databases service does not permit retrieval of information on - * individual databases (only via Collection), and thus passing in a - * name via the `$info` parameter only creates an in-memory object that - * is not necessarily tied to an actual database. - * - * @param Instance $instance the parent DbService\Instance of the database - * @param mixed $info if an array or object, treated as properties to set; - * if a string, treated as the database name - * @return void - * @throws DatabaseNameError if `$info` is not a string, object, or array - */ public function __construct(Instance $instance, $info = null) { $this->setParent($instance); + // Catering for laziness if (is_string($info)) { $info = array('name' => $info); } + return parent::__construct($instance->getService(), $info); } - + /** * Returns name of this database. Because it's so important (i.e. as an * identifier), it will throw an error if not set/empty. - * + * * @return type * @throws Exceptions\DatabaseNameError */ @@ -68,6 +59,7 @@ public function getName() Lang::translate('The database does not have a Url yet') ); } + return $this->name; } @@ -75,7 +67,7 @@ public function primaryKeyField() { return 'name'; } - + /** * Returns the Instance of the database * @@ -99,7 +91,7 @@ public function create($params = array()) $url = $this->getParent()->url('databases'); if (isset($params['name'])) { - $this->name = $params['name']; + $this->name = $params['name']; } $json = json_encode($this->createJson($params)); @@ -118,18 +110,7 @@ public function create($params = array()) */ public function update($params = array()) { - return $this->noUpdate(); - } - - /** - * Deletes a database - * - * @api - * @return \OpenCloud\HttpResponseb - */ - public function delete() - { - return $this->getClient()->delete($this->url())->send(); + return $this->noUpdate(); } /** @@ -143,5 +124,4 @@ protected function createJson(array $params = array()) 'databases' => array($database) ); } - -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Database/Resource/Datastore.php b/rackspace/php-opencloud/lib/OpenCloud/Database/Resource/Datastore.php new file mode 100644 index 000000000..3123fc740 --- /dev/null +++ b/rackspace/php-opencloud/lib/OpenCloud/Database/Resource/Datastore.php @@ -0,0 +1,86 @@ + 'DatastoreVersion' + ); + + /** + * @throws CreateError + */ + public function create($params = array()) + { + return $this->noCreate(); + } + + /** + * @throws UpdateError + */ + public function update($params = array()) + { + return $this->noUpdate(); + } + + /** + * @throws DeleteError + */ + public function delete() + { + return $this->noDelete(); + } + + /** + * Returns a new DatastoreVersion object + * + * @param string $name the version name + * @return Version + */ + public function version($id = null) + { + return $this->getService()->resource('DatastoreVersion', $id, $this); + } + + /** + * Returns a Collection of all versions for this datastore + * + * @return OpenCloud\Common\Collection\PaginatedIterator + */ + public function versionList() + { + return $this->getService()->resourceList('DatastoreVersion', $this->getUrl('versions')); + } +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Database/Resource/DatastoreVersion.php b/rackspace/php-opencloud/lib/OpenCloud/Database/Resource/DatastoreVersion.php new file mode 100644 index 000000000..51fe9d2c2 --- /dev/null +++ b/rackspace/php-opencloud/lib/OpenCloud/Database/Resource/DatastoreVersion.php @@ -0,0 +1,65 @@ + 'Datastore' + ); + + /** + * @throws CreateError + */ + public function create($params = array()) + { + return $this->noCreate(); + } + + /** + * @throws UpdateError + */ + public function update($params = array()) + { + return $this->noUpdate(); + } + + /** + * @throws DeleteError + */ + public function delete() + { + return $this->noDelete(); + } +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Database/Resource/Instance.php b/rackspace/php-opencloud/lib/OpenCloud/Database/Resource/Instance.php index d1f6cd603..e7f09fddf 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Database/Resource/Instance.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Database/Resource/Instance.php @@ -1,29 +1,35 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Database\Resource; -use OpenCloud\Common\PersistentObject; +use OpenCloud\Common\Http\Message\Formatter; use OpenCloud\Common\Lang; use OpenCloud\Common\Exceptions; +use OpenCloud\Common\Resource\NovaResource; use OpenCloud\Compute\Resource\Flavor; use OpenCloud\Database\Service; -use OpenCloud\Common\Http\Message\Formatter; /** * Instance represents an instance of DbService, similar to a Server in a * Compute service */ -class Instance extends PersistentObject +class Instance extends NovaResource { - public $id; public $name; public $status; @@ -33,12 +39,13 @@ class Instance extends PersistentObject public $created; public $updated; public $flavor; - + public $backupRef; + protected static $json_name = 'instance'; protected static $url_resource = 'instances'; - private $_databases; // used to Create databases simultaneously - private $_users; // used to Create users simultaneously + private $_databases; // used to Create databases simultaneously + private $_users; // used to Create users simultaneously /** * Creates a new instance object @@ -46,27 +53,15 @@ class Instance extends PersistentObject * This could use the default constructor, but we want to make sure that * the volume attribute is an object. * - * @param \OpenCloud\DbService $service the DbService object associated - * with this - * @param mixed $info the ID or array of info for the object + * @param \OpenCloud\DbService $service the DbService object associated + * with this + * @param mixed $info the ID or array of info for the object */ - public function __construct(Service $service, $info = null) + public function __construct(Service $service, $info = null) { $this->volume = new \stdClass; - return parent::__construct($service, $info); - } - /** - * Updates a database instance (not permitted) - * - * Update() is not supported by database instances; thus, this always - * throws an exception. - * - * @throws InstanceUpdateError always - */ - public function update($params = array()) - { - return $this->noUpdate(); + return parent::__construct($service, $info); } /** @@ -75,7 +70,7 @@ public function update($params = array()) * @api * @returns \OpenCloud\HttpResponse */ - public function restart() + public function restart() { return $this->action($this->restartJson()); } @@ -87,7 +82,7 @@ public function restart() * @param \OpenCloud\Compute\Flavor $flavor a flavor object * @returns \OpenCloud\HttpResponse */ - public function resize(Flavor $flavor) + public function resize(Flavor $flavor) { return $this->action($this->resizeJson($flavor->id)); } @@ -99,7 +94,7 @@ public function resize(Flavor $flavor) * @param integer $newvolumesize the size of the new volume, in gigabytes * @return \OpenCloud\HttpResponse */ - public function resizeVolume($newvolumesize) + public function resizeVolume($newvolumesize) { return $this->action($this->resizeVolumeJson($newvolumesize)); } @@ -111,10 +106,11 @@ public function resizeVolume($newvolumesize) * @return User the root user, including name and password * @throws InstanceError if HTTP response is not Success */ - public function enableRootUser() + public function enableRootUser() { $response = $this->getClient()->post($this->getUrl('root'))->send(); $body = Formatter::decode($response); + return (isset($body->user)) ? new User($this, $body->user) : false; } @@ -125,10 +121,11 @@ public function enableRootUser() * @return boolean TRUE if the root user is enabled; FALSE otherwise * @throws InstanceError if HTTP status is not Success */ - public function isRootEnabled() + public function isRootEnabled() { $response = $this->getClient()->get($this->url('root'))->send(); $body = Formatter::decode($response); + return !empty($body->rootEnabled); } @@ -138,7 +135,7 @@ public function isRootEnabled() * @param string $name the database name * @return Database */ - public function database($name = '') + public function database($name = '') { return new Database($this, $name); } @@ -146,11 +143,11 @@ public function database($name = '') /** * Returns a new User object * - * @param string $name the user name - * @param array $databases a simple array of database names + * @param string $name the user name + * @param array $databases a simple array of database names * @return User */ - public function user($name = '', $databases = array()) + public function user($name = '', $databases = array()) { return new User($this, $name, $databases); } @@ -160,7 +157,7 @@ public function user($name = '', $databases = array()) * * @return OpenCloud\Common\Collection\PaginatedIterator */ - public function databaseList() + public function databaseList() { return $this->getService()->resourceList('Database', $this->getUrl('databases'), $this); } @@ -170,43 +167,106 @@ public function databaseList() * * @return OpenCloud\Common\Collection\PaginatedIterator */ - public function userList() + public function userList() { return $this->getService()->resourceList('User', $this->getUrl('users'), $this); } + /** + * Returns a Collection of all backups for the instance + * + * @return OpenCloud\Common\Collection\PaginatedIterator + */ + public function backupList() + { + return $this->getService()->resourceList('Backup', $this->getUrl('backups'), $this); + } + + /** + * Creates a backup for the given instance + * + * @api + * @param array $params - an associate array of key/value pairs + * name is required + * description is optional + * @return Backup + */ + public function createBackup($params = array()) + { + if (!isset($params['instanceId'])) { + $params['instanceId'] = $this->id; + } + + $backup = new Backup($this->getService(), $params); + $backup->create($params); + return $backup; + } + + public function populate($info, $setObjects = true) + { + parent::populate($info, $setObjects); + + if (is_object($info)) { + $info = (array) $info; + } + + if (isset($info['restorePoint']['backupRef'])) { + $this->backupRef = $info['restorePoint']['backupRef']; + } + } + /** * Generates the JSON string for Create() * * @return \stdClass */ - protected function createJson() + protected function createJson() { if (empty($this->flavor) || !is_object($this->flavor)) { throw new Exceptions\InstanceFlavorError( Lang::translate('The `flavor` attribute is required and must be a Flavor object') ); } - + if (!isset($this->name)) { throw new Exceptions\InstanceError( Lang::translate('Instance name is required') ); } - + + $out = [ + 'instance' => [ + 'flavorRef' => $this->flavor->links[0]->href, + 'name' => $this->name, + 'volume' => $this->volume + ] + ]; + + if (isset($this->backupRef)) { + $out['instance']['restorePoint'] = [ + 'backupRef' => $this->backupRef + ]; + } + + return (object) $out; + } + + /** + * Generates the JSON string for update() + * + * @return \@stdClass + */ + protected function updateJson($params = array()) + { return (object) array( - 'instance' => (object) array( - 'flavorRef' => $this->flavor->links[0]->href, - 'name' => $this->name, - 'volume' => $this->volume - ) + self::$json_name => $params ); } /** * Generates the JSON object for Restart */ - private function restartJson() + private function restartJson() { return (object) array('restart' => new \stdClass); } @@ -214,7 +274,7 @@ private function restartJson() /** * Generates the JSON object for Resize */ - private function resizeJson($flavorRef) + private function resizeJson($flavorRef) { return (object) array( 'resize' => (object) array('flavorRef' => $flavorRef) @@ -224,13 +284,12 @@ private function resizeJson($flavorRef) /** * Generates the JSON object for ResizeVolume */ - private function resizeVolumeJson($size) + private function resizeVolumeJson($size) { return (object) array( 'resize' => (object) array( - 'volume' => (object) array('size' => $size) - ) + 'volume' => (object) array('size' => $size) + ) ); } - } diff --git a/rackspace/php-opencloud/lib/OpenCloud/Database/Resource/User.php b/rackspace/php-opencloud/lib/OpenCloud/Database/Resource/User.php index 58867a832..8939fa003 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Database/Resource/User.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Database/Resource/User.php @@ -1,35 +1,41 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Database\Resource; -use OpenCloud\Common\PersistentObject; use OpenCloud\Common\Exceptions; use OpenCloud\Common\Lang; +use OpenCloud\Common\Resource\PersistentResource; /** * This class represents a User in the Rackspace "Red Dwarf" * database-as-a-service product. */ -class User extends PersistentObject +class User extends PersistentResource { - - /** - * @var string $name the user name - * @var string $password the user's password - * @var array $databases a list of database names assigned to the user - */ + /** @var string The user name */ public $name; + + /** @var string The user's password */ public $password; + + /** @var array A list of database names assigned to the user */ public $databases = array(); - + protected static $json_name = 'user'; protected static $url_resource = 'users'; @@ -47,9 +53,9 @@ class User extends PersistentObject * is not necessarily tied to an actual database. * * @param Instance $instance the parent DbService\Instance of the database - * @param mixed $info if an array or object, treated as properties to set; - * if a string, treated as the database name - * @param array $db a list of database names to associate with the User + * @param mixed $info if an array or object, treated as properties to set; + * if a string, treated as the database name + * @param array $db a list of database names to associate with the User * @return void * @throws UserNameError if `$info` is not a string, object, or array */ @@ -60,7 +66,7 @@ public function __construct(Instance $instance, $info = null, $db = array()) if (!empty($db)) { $this->databases = $db; } - + // Lazy... if (is_string($info)) { $info = array('name' => $info); @@ -68,11 +74,11 @@ public function __construct(Instance $instance, $info = null, $db = array()) return parent::__construct($instance->getService(), $info); } - + /** * Returns name of this user. Because it's so important (i.e. as an * identifier), it will throw an error if not set/empty. - * + * * @return type * @throws Exceptions\DatabaseNameError */ @@ -83,9 +89,10 @@ public function getName() Lang::translate('This user does not have a name yet') ); } + return $this->name; } - + /** * {@inheritDoc} */ @@ -94,55 +101,80 @@ public function primaryKeyField() return 'name'; } - /** - * Adds a new database to the list of databases for the user - * - * @api - * @param string $dbname the database name to be added - * @return void - */ - public function addDatabase($dbname) + /** + * Adds a new database to the list of databases for the user + * + * @api + * @param string $dbname the database name to be added + * @return void + */ + public function addDatabase($dbname) { - $this->databases[] = $dbname; - } - - /** - * {@inheritDoc} - */ - public function update($params = array()) + $this->databases[] = $dbname; + } + + /** + * {@inheritDoc} + */ + public function update($params = array()) + { + return $this->noUpdate(); + } + + /** + * Deletes a database user + * + * @api + * @return \OpenCloud\HttpResponse + * @throws UserDeleteError if HTTP response is not Success + */ + public function delete() { - return $this->noUpdate(); - } - - /** - * Deletes a database user - * - * @api - * @return \OpenCloud\HttpResponse - * @throws UserDeleteError if HTTP response is not Success - */ - public function delete() + return $this->getClient()->delete($this->url())->send(); + } + + /** + * {@inheritDoc} + */ + protected function createJson() { - return $this->getClient()->delete($this->url())->send(); - } - - /** - * {@inheritDoc} - */ - protected function createJson() - { $user = (object) array( 'name' => $this->name, 'password' => $this->password, 'databases' => array() ); - - foreach ($this->databases as $dbName) { - $user->databases[] = (object) array('name' => $dbName); - } - return (object) array( + foreach ($this->databases as $dbName) { + $user->databases[] = (object) array('name' => $dbName); + } + + return (object) array( 'users' => array($user) ); - } + } + + /** + * Grant access to a set of one or more databases to a user. + * + * @param []string $databases An array of one or more database names that this user will be granted access to. For + * example, ['foo', 'bar'] or ['baz'] are valid inputs. + * + * @return \Guzzle\Http\Message\Response + */ + public function grantDbAccess(array $databases) + { + $json = []; + + foreach ($databases as $database) { + $json[] = ['name' => $database]; + } + + $json = ['databases' => $json]; + + $url = $this->getUrl('databases'); + $headers = self::getJsonHeader(); + $body = json_encode($json); + + return $this->getClient()->put($url, $headers, $body)->send(); + } } diff --git a/rackspace/php-opencloud/lib/OpenCloud/Database/Service.php b/rackspace/php-opencloud/lib/OpenCloud/Database/Service.php index 16e8d397b..31504d208 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Database/Service.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Database/Service.php @@ -1,20 +1,31 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Database; +use Guzzle\Http\ClientInterface; use OpenCloud\Common\Service\NovaService; -use OpenCloud\OpenStack; +use OpenCloud\Database\Resource\Instance; +use OpenCloud\Database\Resource\Configuration; +use OpenCloud\Database\Resource\Datastore; +use OpenCloud\Database\Resource\Backup; /** - * The Rackspace Database As A Service (aka "Red Dwarf") + * The Rackspace Database service */ class Service extends NovaService { @@ -22,41 +33,102 @@ class Service extends NovaService const DEFAULT_NAME = 'cloudDatabases'; /** - * Returns a list of flavors + * Returns an Instance * - * just call the parent FlavorList() method, but pass FALSE - * because the /flavors/detail resource is not supported + * @param string $id the ID of the instance to retrieve + * @return \OpenCloud\Database\Resource\Instance + */ + public function instance($id = null) + { + return $this->resource('Instance', $id); + } + + /** + * Returns a Collection of Instance objects * - * @api - * @return \OpenCloud\Compute\FlavorList + * @param array $params + * @return \OpenCloud\Common\Collection\PaginatedIterator */ - public function flavorList($details = false, array $filter = array()) + public function instanceList($params = array()) { - return parent::flavorList(false); + $url = clone $this->getUrl(); + $url->addPath(Instance::resourceName())->setQuery($params); + + return $this->resourceList('Instance', $url); } /** - * Creates a Instance object + * Returns a Configuration * - * @api - * @param string $id the ID of the instance to retrieve - * @return DbService\Instance + * @param string $id the ID of the configuration to retrieve + * @return \OpenCloud\Database\Resource\Configuration */ - public function instance($id = null) + public function configuration($id = null) { - return new Resource\Instance($this, $id); + return $this->resource('Configuration', $id); } /** - * Creates a Collection of Instance objects + * Returns a Collection of Configuration objects * - * @api - * @param array $params array of parameters to pass to the request as - * query strings - * @return Collection + * @param array $params + * @return \OpenCloud\Common\Collection\PaginatedIterator */ - public function instanceList($params = array()) + public function configurationList($params = array()) + { + $url = clone $this->getUrl(); + $url->addPath(Configuration::resourceName())->setQuery($params); + + return $this->resourceList('Configuration', $url); + } + + /** + * Returns a Datastore + * + * @param string $id the ID of the datastore to retrieve + * @return \OpenCloud\Database\Resource\Datastore + */ + public function datastore($id = null) { - return $this->collection('OpenCloud\Database\Resource\Instance', null, null, $params); + return $this->resource('Datastore', $id); + } + + /** + * Returns a Collection of Datastore objects + * + * @param array $params + * @return \OpenCloud\Common\Collection\PaginatedIterator + */ + public function datastoreList($params = array()) + { + $url = clone $this->getUrl(); + $url->addPath(Datastore::resourceName())->setQuery($params); + + return $this->resourceList('Datastore', $url); + } + + /** + * Returns a Backup + * + * @param string $id the ID of the backup to retrieve + * @return \OpenCloud\Database\Resource\Backup + */ + public function backup($id = null) + { + return $this->resource('Backup', $id); + } + + /** + * Returns a Collection of Backup objects + * + * @param array $params + * @return \OpenCloud\Common\Collection\PaginatedIterator + */ + public function backupList($params = array()) + { + $url = clone $this->getUrl(); + $url->addPath(Backup::resourceName())->setQuery($params); + + return $this->resourceList('Backup', $url); } } diff --git a/rackspace/php-opencloud/lib/OpenCloud/Identity/Constants/User.php b/rackspace/php-opencloud/lib/OpenCloud/Identity/Constants/User.php index c15de23e6..2d944d4ab 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Identity/Constants/User.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Identity/Constants/User.php @@ -1,12 +1,25 @@ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Identity\Resource; @@ -35,7 +43,7 @@ class Role extends PersistentObject private $description; protected static $url_resource = 'OS-KSADM/roles'; - protected static $json_name = 'role'; + protected static $json_name = 'role'; /** * @param $id Sets the ID @@ -84,5 +92,4 @@ public function getDescription() { return $this->description; } - -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Identity/Resource/Tenant.php b/rackspace/php-opencloud/lib/OpenCloud/Identity/Resource/Tenant.php index 152910a79..6bf300a89 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Identity/Resource/Tenant.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Identity/Resource/Tenant.php @@ -1,10 +1,18 @@ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Identity\Resource; @@ -34,7 +42,7 @@ class Tenant extends PersistentObject private $enabled; protected static $url_resource = 'tenants'; - protected static $json_name = 'tenants'; + protected static $json_name = 'tenants'; /** * @param $id Sets the ID @@ -99,5 +107,4 @@ public function isEnabled() { return $this->enabled === true; } - -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Identity/Resource/Token.php b/rackspace/php-opencloud/lib/OpenCloud/Identity/Resource/Token.php index 0ebbf8f82..4a72e4816 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Identity/Resource/Token.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Identity/Resource/Token.php @@ -1,10 +1,18 @@ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Identity\Resource; @@ -68,5 +76,4 @@ public function hasExpired() { return time() >= strtotime($this->expires); } - -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Identity/Resource/User.php b/rackspace/php-opencloud/lib/OpenCloud/Identity/Resource/User.php index 9cdd270ab..736fbd09f 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Identity/Resource/User.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Identity/Resource/User.php @@ -1,10 +1,18 @@ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Identity\Resource; @@ -12,6 +20,7 @@ use OpenCloud\Common\Collection\PaginatedIterator; use OpenCloud\Common\Http\Message\Formatter; use OpenCloud\Common\PersistentObject; +use OpenCloud\Rackspace; /** * User class which encapsulates functionality for a user. @@ -29,7 +38,7 @@ class User extends PersistentObject /** @var string The default region for this region. Can be ORD, DFW, IAD, LON, HKG or SYD */ private $defaultRegion; - /** @var string */ + /** @var string */ private $domainId; /** @var int The ID of this user */ @@ -47,17 +56,30 @@ class User extends PersistentObject /** @var string The string password for this user */ private $password; - protected $createKeys = array('username', 'email', 'enabled'); + protected $createKeys = array('username', 'email', 'enabled', 'password'); protected $updateKeys = array('username', 'email', 'enabled', 'RAX-AUTH:defaultRegion', 'RAX-AUTH:domainId', 'id'); protected $aliases = array( + 'name' => 'username', 'RAX-AUTH:defaultRegion' => 'defaultRegion', 'RAX-AUTH:domainId' => 'domainId', 'OS-KSADM:password' => 'password' ); protected static $url_resource = 'users'; - protected static $json_name = 'user'; + protected static $json_name = 'user'; + + public function createJson() + { + $json = parent::createJson(); + + if ($this->getClient() instanceof Rackspace) { + $json->user->username = $json->user->name; + unset($json->user->name); + } + + return $json; + } /** * @param $region Set the default region @@ -164,7 +186,7 @@ public function isEnabled() } /** - * @param $username Set the username + * @param $password Set the password */ public function setPassword($password) { @@ -172,13 +194,16 @@ public function setPassword($password) } /** - * @return string Get the username + * @return string Get the password */ public function getPassword() { return $this->password; } + /** + * @return string + */ public function primaryKeyField() { return 'id'; @@ -192,11 +217,12 @@ public function updateJson($params = array()) $array[$key] = $this->$key; } } + return (object) array('user' => $array); } /** - * This operation will set this user's password to a new value. + * This operation will set the user's password to a new value. * * @param $newPassword The new password to use for this user * @return \Guzzle\Http\Message\Response @@ -204,7 +230,7 @@ public function updateJson($params = array()) public function updatePassword($newPassword) { $array = array( - 'username' => $this->username, + 'username' => $this->username, 'OS-KSADM:password' => $newPassword ); @@ -329,5 +355,4 @@ public function update($params = array()) return $this->getClient()->post($this->getUrl(), self::getJsonHeader(), $json)->send(); } - -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Identity/Service.php b/rackspace/php-opencloud/lib/OpenCloud/Identity/Service.php index fdf628fde..4e34f1e24 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Identity/Service.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Identity/Service.php @@ -1,21 +1,30 @@ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Identity; use Guzzle\Http\ClientInterface; -use Guzzle\Http\Url; +use OpenCloud\Common\Base; use OpenCloud\Common\Collection\PaginatedIterator; use OpenCloud\Common\Collection\ResourceIterator; use OpenCloud\Common\Http\Message\Formatter; use OpenCloud\Common\Service\AbstractService; use OpenCloud\Identity\Constants\User as UserConst; +use OpenCloud\OpenStack; /** * Class responsible for working with Rackspace's Cloud Identity service. @@ -24,7 +33,6 @@ */ class Service extends AbstractService { - /** * Factory method which allows for easy service creation * @@ -34,6 +42,11 @@ class Service extends AbstractService public static function factory(ClientInterface $client) { $identity = new self(); + + if (($client instanceof Base || $client instanceof OpenStack) && $client->hasLogger()) { + $identity->setLogger($client->getLogger()); + } + $identity->setClient($client); $identity->setEndpoint(clone $client->getAuthUrl()); @@ -121,6 +134,7 @@ public function createUser(array $params) { $user = $this->resource('User'); $user->create($params); + return $user; } @@ -177,6 +191,7 @@ public function revokeToken($tokenId) { $token = $this->resource('Token'); $token->setId($tokenId); + return $token->delete(); } @@ -199,5 +214,4 @@ public function getTenants() ), $body->tenants); } } - -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Image/Enum/Document.php b/rackspace/php-opencloud/lib/OpenCloud/Image/Enum/Document.php new file mode 100644 index 000000000..d60365c5f --- /dev/null +++ b/rackspace/php-opencloud/lib/OpenCloud/Image/Enum/Document.php @@ -0,0 +1,25 @@ +data = $data; + } + + /** + * @return array + */ + public function getData() + { + return $this->data; + } + + /** + * @param $id + */ + public function setId($id) + { + $this->id = (string) $id; + } + + /** + * @return string + */ + public function getId() + { + return $this->id; + } + + /** + * Sets a value to a particular offset. + * + * @param mixed $offset + * @param mixed $value + */ + public function offsetSet($offset, $value) + { + if ($offset === null) { + $this->data[] = $value; + } else { + $this->data[$offset] = $value; + } + } + + /** + * Checks to see whether a particular offset key exists. + * + * @param mixed $offset + * @return bool + */ + public function offsetExists($offset) + { + return array_key_exists($offset, $this->data); + } + + /** + * Unset a particular key. + * + * @param mixed $offset + */ + public function offsetUnset($offset) + { + unset($this->data[$offset]); + } + + /** + * Get the value for a particular offset key. + * + * @param mixed $offset + * @return mixed|null + */ + public function offsetGet($offset) + { + return $this->offsetExists($offset) ? $this->data[$offset] : null; + } +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Image/Resource/Image.php b/rackspace/php-opencloud/lib/OpenCloud/Image/Resource/Image.php new file mode 100644 index 000000000..f24eb510b --- /dev/null +++ b/rackspace/php-opencloud/lib/OpenCloud/Image/Resource/Image.php @@ -0,0 +1,226 @@ +getService()->getImageSchema(); + + $document = new JsonDocument(); + + foreach ($params as $propertyName => $value) { + // find property object + if (!($property = $schema->getProperty($propertyName))) { + // check whether additional properties are found + if (false === ($property = $schema->validateAdditionalProperty($value))) { + throw new \RuntimeException( + 'If a property does not exist in the schema, the `additionalProperties` property must be set' + ); + } + } + + // do validation checks + $property->setName($propertyName); + $property->setValue($value); + $property->validate(); + + // decide operation type + if (!$value) { + $operationType = OperationType::REMOVE; + } elseif ($this->offsetExists($propertyName)) { + $operationType = OperationType::REPLACE; + } else { + $operationType = $schema->decideOperationType($property); + } + + // create JSON-patch operation + $operation = JsonOperation::factory($schema, $property, $operationType); + + // add to JSON document + $document->addOperation($operation); + } + + // create request + $body = $document->toString(); + + return $this->getClient() + ->patch($this->getUrl(), $this->getPatchHeaders(), $body) + ->send(); + } + + /** + * Refresh this resource + * + * @return \Guzzle\Http\Message\Response + */ + public function refresh() + { + $response = $this->getClient()->get($this->getUrl())->send(); + + $this->setData($response->json()); + + return $response; + } + + /** + * Delete this resource + * + * @return \Guzzle\Http\Message\Response + */ + public function delete() + { + return $this->getClient()->delete($this->getUrl())->send(); + } + + /** + * List the members of this image + * + * @param array $params + * @return mixed + */ + public function listMembers(array $params = array()) + { + $url = clone $this->getUrl(); + $url->addPath(Member::resourceName())->setQuery($params); + + return $this->getService()->resourceList('Member', $url, $this); + } + + /** + * Iterator use only + * + * @param $data + * @return mixed + */ + public function member($data) + { + $data = (array) $data; + + $member = $this->getService()->resource('Member', null, $this); + $member->setData($data); + + if (isset($data['member_id'])) { + $member->setId($data['member_id']); + } + + return $member; + } + + /** + * Get a member belonging to this image + * + * @param $memberId + * @return mixed + */ + public function getMember($memberId) + { + $url = clone $this->getUrl(); + $url->addPath('members'); + $url->addPath((string) $memberId); + + $data = $this->getClient()->get($url)->send()->json(); + + return $this->member($data); + } + + /** + * Add a member to this image + * + * @param $tenantId + * @return \Guzzle\Http\Message\Response + */ + public function createMember($tenantId) + { + $url = $this->getUrl(); + $url->addPath('members'); + + $json = json_encode(array('member' => $tenantId)); + return $this->getClient()->post($url, self::getJsonHeader(), $json)->send(); + } + + /** + * Delete a member from this image + * + * @param $tenantId + * @return \Guzzle\Http\Message\Response + */ + public function deleteMember($tenantId) + { + $url = $this->getUrl(); + $url->addPath('members'); + $url->addPath((string)$tenantId); + + return $this->getClient()->delete($url)->send(); + } + + /** + * Add a tag to this image + * + * @param string $tag + * @return \Guzzle\Http\Message\Response + */ + public function addTag($tag) + { + $url = clone $this->getUrl(); + $url->addPath('tags')->addPath((string) $tag); + + return $this->getClient()->put($url)->send(); + } + + /** + * Delete a tag from this image + * + * @param $tag + * @return \Guzzle\Http\Message\Response + */ + public function deleteTag($tag) + { + $url = clone $this->getUrl(); + $url->addPath('tags')->addPath((string) $tag); + + return $this->getClient()->delete($url)->send(); + } +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Image/Resource/ImageInterface.php b/rackspace/php-opencloud/lib/OpenCloud/Image/Resource/ImageInterface.php new file mode 100644 index 000000000..ceb35f5c3 --- /dev/null +++ b/rackspace/php-opencloud/lib/OpenCloud/Image/Resource/ImageInterface.php @@ -0,0 +1,40 @@ +operations; + } + + /** + * Add a new JSON operation to the document + * + * @param Operation $operation + */ + public function addOperation(Operation $operation) + { + $this->operations[] = $operation; + } + + /** + * Encode all the operations into a flat structure for HTTP transfer + * + * @return string + */ + public function getResponseBody() + { + $this->validateOperations(); + + return Encoder::encode($this->operations); + } + + /** + * Ensure each operation is valid + */ + protected function validateOperations() + { + foreach ($this->operations as $operation) { + $operation->validate(); + } + } + + /** + * Cast this document as a string + * + * @return string + */ + public function toString() + { + return (string) $this->getResponseBody(); + } +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Image/Resource/JsonPatch/Encoder.php b/rackspace/php-opencloud/lib/OpenCloud/Image/Resource/JsonPatch/Encoder.php new file mode 100644 index 000000000..cac1a031a --- /dev/null +++ b/rackspace/php-opencloud/lib/OpenCloud/Image/Resource/JsonPatch/Encoder.php @@ -0,0 +1,66 @@ + '~0', + '/' => '~1' + ); + + /** + * Encode the + * @param array $operations + * @return string + */ + public static function encode(array $operations) + { + $lines = array(); + + foreach ($operations as $operation) { + $lines[] = sprintf( + '{"%s": "%s", "%s": "%s", "%s": "%s"}', + DocumentEnum::OP, $operation->getType(), + DocumentEnum::PATH, $operation->getPath(), + DocumentEnum::VALUE, self::transform($operation->getValue()) + ); + } + + return sprintf('[%s]', implode($lines, ',')); + } + + /** + * Search a given string and transform any reserved characters into their safe version + * + * @param $value + * @return string + */ + public static function transform($value) + { + return strtr((string) $value, self::$transformations); + } +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Image/Resource/JsonPatch/Operation.php b/rackspace/php-opencloud/lib/OpenCloud/Image/Resource/JsonPatch/Operation.php new file mode 100644 index 000000000..583e4ee9f --- /dev/null +++ b/rackspace/php-opencloud/lib/OpenCloud/Image/Resource/JsonPatch/Operation.php @@ -0,0 +1,148 @@ +setType($operationType); + $operation->setSchema($schema); + $operation->setPath($property->getPath()); + $operation->setValue($property->getValue()); + + return $operation; + } + + /** + * @param $type string + */ + public function setType($type) + { + $this->type = $type; + } + + /** + * @return string + */ + public function getType() + { + return $this->type; + } + + /** + * @param Schema $schema + */ + public function setSchema(Schema $schema) + { + $this->schema = $schema; + } + + /** + * @return Schema + */ + public function getSchema() + { + return $this->schema; + } + + /** + * @param $path + */ + public function setPath($path) + { + $this->path = $path; + } + + /** + * @return string + */ + public function getPath() + { + return $this->path; + } + + /** + * @param $value + */ + public function setValue($value) + { + $this->value = $value; + } + + /** + * @return mixed + */ + public function getValue() + { + return $this->value; + } + + /** + * Validates that this operation is of an allowed type + * + * @throws \RuntimeException if not + */ + public function validate() + { + if (!in_array($this->type, $this->allowedTypes)) { + throw new \RuntimeException(sprintf("%s is not an allowed JSON PATCH operation type", $this->type)); + } + } +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Image/Resource/Member.php b/rackspace/php-opencloud/lib/OpenCloud/Image/Resource/Member.php new file mode 100644 index 000000000..5543dbb1c --- /dev/null +++ b/rackspace/php-opencloud/lib/OpenCloud/Image/Resource/Member.php @@ -0,0 +1,93 @@ +allowedStates)) { + throw new \InvalidArgumentException( + sprintf('Status must be one of these defined types: %s', implode($this->allowedStates, ',')) + ); + } + + $json = json_encode(array('status' => $status)); + + $request = $this->getClient()->put($this->getUrl(), self::getJsonHeader(), $json); + + try { + return $request->send(); + } catch (BadResponseException $e) { + $response = $e->getResponse(); + + switch ($response->getStatusCode()) { + case 403: + $exception = ForbiddenOperationException::factory($e); + break; + case 404: + $exception = ResourceNotFoundException::factory($e); + break; + } + + throw isset($exception) ? $exception : $e; + } + } + + /** + * @return \Guzzle\Http\Message\Response + */ + public function delete() + { + return $this->getClient()->delete($this->getUrl())->send(); + } +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Image/Resource/Schema/AbstractSchemaItem.php b/rackspace/php-opencloud/lib/OpenCloud/Image/Resource/Schema/AbstractSchemaItem.php new file mode 100644 index 000000000..ae7d048f6 --- /dev/null +++ b/rackspace/php-opencloud/lib/OpenCloud/Image/Resource/Schema/AbstractSchemaItem.php @@ -0,0 +1,38 @@ +setName(self::stockProperty($data, SchemaEnum::NAME)); + $property->setDescription(self::stockProperty($data, SchemaEnum::DESCRIPTION)); + $property->setType(self::stockProperty($data, SchemaEnum::TYPE)); + $property->setEnum(self::stockProperty($data, SchemaEnum::ENUM)); + $property->setPattern(self::stockProperty($data, SchemaEnum::PATTERN)); + + if (isset($data[SchemaEnum::ITEMS])) { + // handle sub-schemas + $property->setItems($data[SchemaEnum::ITEMS]); + } + + return $property; + } + + /** + * @param $name + */ + public function setName($name) + { + $this->name = $name; + } + + /** + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * @param $description + */ + public function setDescription($description) + { + $this->description = $description; + } + + /** + * @return string + */ + public function getDescription() + { + return $this->description; + } + + /** + * @param $type + */ + public function setType($type) + { + $this->type = $type; + } + + /** + * @return string + */ + public function getType() + { + return $this->type; + } + + /** + * @param $enum + */ + public function setEnum($enum) + { + $this->enum = $enum; + } + + /** + * @return array + */ + public function getEnum() + { + return $this->enum; + } + + /** + * @param $pattern + */ + public function setPattern($pattern) + { + $this->pattern = $pattern; + } + + /** + * @return string + */ + public function getPattern() + { + return $this->pattern; + } + + /** + * @param $value + */ + public function setValue($value) + { + $this->value = $value; + } + + /** + * @return mixed + */ + public function getValue() + { + return $this->value; + } + + /** + * @param $data + */ + public function setItems($data) + { + $this->items = Schema::factory($data); + } + + /** + * @return array + */ + public function getItems() + { + return $this->items; + } + + /** + * Prepare the given pattern for Regex functions + * + * @param $pattern + * @return string + */ + protected function preparePattern($pattern) + { + return self::DELIMETER . (string) $pattern . self::DELIMETER; + } + + /** + * Validate the current value and ensure that it adheres to correct formatting, etc. + * + * @return bool + */ + public function validate() + { + // deal with enumerated types + if (!empty($this->enum)) { + return in_array($this->value, $this->enum); + } + + // handle patterns + if ($this->pattern) { + return (bool) preg_match($this->preparePattern($this->pattern), $this->value); + } + + // handle type + if ($this->type) { + return $this->type === gettype($this->value); + } + + return true; + } + + /** + * Get the JSON pointer for this property + * + * @return string + */ + public function getPath() + { + return sprintf("/%s", Encoder::transform($this->name)); + } +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Image/Resource/Schema/Schema.php b/rackspace/php-opencloud/lib/OpenCloud/Image/Resource/Schema/Schema.php new file mode 100644 index 000000000..878a0c731 --- /dev/null +++ b/rackspace/php-opencloud/lib/OpenCloud/Image/Resource/Schema/Schema.php @@ -0,0 +1,193 @@ +setName(self::stockProperty($data, SchemaEnum::NAME)); + + if (isset($data[SchemaEnum::LINKS])) { + $schema->setLinks($data[SchemaEnum::LINKS]); + } + + if (isset($data[SchemaEnum::PROPERTIES])) { + $schema->setProperties($data[SchemaEnum::PROPERTIES]); + } + + if (isset($data[SchemaEnum::ADDITIONAL_PROPERTIES])) { + $schema->setAdditionalProperties($data[SchemaEnum::ADDITIONAL_PROPERTIES]); + } + + return $schema; + } + + /** + * @param string $name + */ + public function setName($name) + { + $this->name = $name; + } + + /** + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * @param array $properties + */ + public function setProperties(array $properties) + { + foreach ($properties as $name => $array) { + $array[SchemaEnum::NAME] = $name; + $this->properties[$name] = Property::factory($array); + } + } + + /** + * @return array + */ + public function getProperties() + { + return $this->properties; + } + + /** + * @param array $properties + */ + public function setAdditionalProperties(array $properties) + { + $this->additionalProperties = $properties; + } + + /** + * @return bool|Property + */ + public function getAdditionalProperties() + { + if (!empty($this->additionalProperties)) { + return Property::factory($this->additionalProperties); + } + + return false; + } + + /** + * @param array $links + */ + public function setLinks(array $links) + { + $this->links = $links; + } + + /** + * @return array + */ + public function getLinks() + { + return $this->links; + } + + /** + * Check whether a property exists + * + * @param $property The name of the property + * @return bool + */ + public function propertyExists($property) + { + return isset($this->properties[$property]); + } + + /** + * Retrieve a property + * + * @param $property The name of the property + * @return null|Property + */ + public function getProperty($property) + { + return $this->propertyExists($property) ? $this->properties[$property] : null; + } + + /** + * Based on this schema, decide the most appropriate operation type for a given property + * + * @param Property $property The property being performed on + * @return string + */ + public function decideOperationType(Property $property) + { + $name = $property->getName(); + + return ($this->propertyExists($name)) ? OperationType::REPLACE : OperationType::ADD; + } + + /** + * Check whether an additional property is allowed and its type is valid + * + * @param $value The value trying to be set + * @return bool|Property + */ + public function validateAdditionalProperty($value) + { + if ($property = $this->getAdditionalProperties()) { + $property->setValue($value); + + return ($property->validate() === true) ? $property : false; + } + + return false; + } +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Image/Service.php b/rackspace/php-opencloud/lib/OpenCloud/Image/Service.php new file mode 100644 index 000000000..f0ed4d72a --- /dev/null +++ b/rackspace/php-opencloud/lib/OpenCloud/Image/Service.php @@ -0,0 +1,137 @@ +getUrl(); + $url->addPath(Image::resourceName())->setQuery($params); + + return $this->resourceList('Image', $url); + } + + /** + * Returns details for a specific image. + * + * @param $imageId + * @return object + */ + public function getImage($imageId) + { + $image = $this->resource('Image'); + $image->setId($imageId); + $image->refresh(); + + return $image; + } + + /** + * For iterator use only. + * + * @param $data + * @return object + */ + public function image($data) + { + $image = $this->resource('Image'); + $image->setData((array) $data); + + return $image; + } + + /** + * A convenience method which returns the URL needed to retrieve schemas. + * + * @param $path + * @return \Guzzle\Http\Url + */ + protected function getSchemaUrl($path) + { + $url = clone $this->getUrl(); + + return $url->addPath('schemas')->addPath($path); + } + + /** + * Return a JSON schema for a collection of image resources + * + * @return Schema + */ + public function getImagesSchema() + { + $data = $this->getClient()->get($this->getSchemaUrl('images'))->send()->json(); + + return Schema::factory($data); + } + + /** + * Return a JSON schema for an individual image resource + * + * @return Schema + */ + public function getImageSchema() + { + $data = $this->getClient()->get($this->getSchemaUrl('image'))->send()->json(); + + return Schema::factory($data); + } + + /** + * Return a JSON schema for a collection of member resources + * + * @return Schema + */ + public function getMembersSchema() + { + $data = $this->getClient()->get($this->getSchemaUrl('members'))->send()->json(); + + return Schema::factory($data); + } + + /** + * Return a JSON schema for an individual member resource + * + * @return Schema + */ + public function getMemberSchema() + { + $data = $this->getClient()->get($this->getSchemaUrl('member'))->send()->json(); + + return Schema::factory($data); + } +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Collection/LoadBalancerIterator.php b/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Collection/LoadBalancerIterator.php new file mode 100644 index 000000000..7febf1af3 --- /dev/null +++ b/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Collection/LoadBalancerIterator.php @@ -0,0 +1,39 @@ +getQuery(); + $query['limit'] = $query['limit'] + 1; + $url->setQuery($query); + + return $url; + } + + public function updateMarkerToCurrent() + { + $this->setMarkerFromElement($this->nextElement); + } + + public function parseResponseBody($body) + { + $response = parent::parseResponseBody($body); + + if (count($response) >= $this->getOption('limit.page')) { + // Pop last element and save (we will need it for the next marker) + $this->nextElement = array_pop($response); + } + + return $response; + } +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Enum/IpType.php b/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Enum/IpType.php new file mode 100644 index 000000000..b30bf7c39 --- /dev/null +++ b/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Enum/IpType.php @@ -0,0 +1,35 @@ +id)) { + return false; + } + + try { + return parent::refresh($id, $url); + } catch (ClientErrorResponseException $e) { + return false; + } + } + + protected function createJson() + { + $object = new \stdClass; + + foreach ($this->createKeys as $item) { + $object->$item = $this->$item; + } + + if ($top = $this->jsonName()) { + $object = array($top => $object); + } + + return $object; + } + + protected function updateJson($params = array()) + { + return $this->createJson(); + } + + public function name() + { + $classArray = explode('\\', get_class($this)); + + return method_exists($this->getParent(), 'id') + ? sprintf('%s-%s', end($classArray), $this->getParent()->id()) + : parent::name(); + } +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/Access.php b/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/Access.php index 4f64042f4..2af0cb41c 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/Access.php +++ b/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/Access.php @@ -1,72 +1,63 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\LoadBalancer\Resource; /** - * The access list management feature allows fine-grained network access - * controls to be applied to the load balancer's virtual IP address. A single IP - * address, multiple IP addresses, or entire network subnets can be added as a - * networkItem. Items that are configured with the ALLOW type will always take - * precedence over items with the DENY type. To reject traffic from all items - * except for those with the ALLOW type, add a networkItem with an address of + * The access list management feature allows fine-grained network access + * controls to be applied to the load balancer's virtual IP address. A single IP + * address, multiple IP addresses, or entire network subnets can be added as a + * networkItem. Items that are configured with the ALLOW type will always take + * precedence over items with the DENY type. To reject traffic from all items + * except for those with the ALLOW type, add a networkItem with an address of * "0.0.0.0/0" and a DENY type. */ -class Access extends SubResource +class Access extends NonIdUriResource { - public $id; - + /** * Type of item to add: - * ALLOW - Specifies items that will always take precedence over items with + * ALLOW - Specifies items that will always take precedence over items with * the DENY type. * DENY - Specifies items to which traffic can be denied. - * - * @var string + * + * @var string */ public $type; - + /** * IP address for item to add to access list. - * - * @var string + * + * @var string */ public $address; - + protected static $json_name = "accessList"; + protected static $json_collection_name = "accessList"; protected static $url_resource = "accesslist"; + protected $createKeys = array( - 'type', + 'type', 'address' ); - public function update($params = array()) - { - return $this->noUpdate(); - } - - protected function createJson() + public function update($params = array()) { - $object = new \stdClass; - - foreach ($this->createKeys as $item) { - $object->$item = $this->$item; - } - - if ($top = $this->jsonName()) { - $object = array($top => array($object)); - } - - return $object; + return $this->noUpdate(); } - -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/Algorithm.php b/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/Algorithm.php index b7797e9bb..9011d0f1e 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/Algorithm.php +++ b/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/Algorithm.php @@ -1,46 +1,51 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\LoadBalancer\Resource; /** - * All load balancers utilize an algorithm that defines how traffic should be - * directed between back-end nodes. The default algorithm for newly created load - * balancers is RANDOM, which can be overridden at creation time or changed - * after the load balancer has been initially provisioned. The algorithm name is - * to be constant within a major revision of the load balancing API, though new - * algorithms may be created with a unique algorithm name within a given major + * All load balancers utilize an algorithm that defines how traffic should be + * directed between back-end nodes. The default algorithm for newly created load + * balancers is RANDOM, which can be overridden at creation time or changed + * after the load balancer has been initially provisioned. The algorithm name is + * to be constant within a major revision of the load balancing API, though new + * algorithms may be created with a unique algorithm name within a given major * revision of the service API. - * + * * Accepted options are: - * - * * LEAST_CONNECTIONS: The node with the lowest number of connections will + * + * * LEAST_CONNECTIONS: The node with the lowest number of connections will * receive requests. - * + * * * RANDOM: Back-end servers are selected at random. - * + * * * ROUND_ROBIN: Connections are routed to each of the back-end servers in turn. - * - * * WEIGHTED_LEAST_CONNECTIONS: Each request will be assigned to a node based - * on the number of concurrent connections to the node and its weight. - * - * * WEIGHTED_ROUND_ROBIN: A round robin algorithm, but with different - * proportions of traffic being directed to the back-end nodes. Weights + * + * * WEIGHTED_LEAST_CONNECTIONS: Each request will be assigned to a node based + * on the number of concurrent connections to the node and its weight. + * + * * WEIGHTED_ROUND_ROBIN: A round robin algorithm, but with different + * proportions of traffic being directed to the back-end nodes. Weights * must be defined as part of the load balancer's node configuration. */ -class Algorithm extends Readonly +class Algorithm extends ReadOnlyResource { - public $name; + protected static $json_name = 'algorithm'; protected static $url_resource = 'loadbalancers/algorithms'; - } diff --git a/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/AllowedDomain.php b/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/AllowedDomain.php index 8493c730a..1eb52fb10 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/AllowedDomain.php +++ b/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/AllowedDomain.php @@ -1,34 +1,38 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\LoadBalancer\Resource; /** - * The allowed domains are restrictions set for the allowed domain names used - * for adding load balancer nodes. In order to submit a domain name as an address - * for the load balancer node to add, the user must verify that the domain is + * The allowed domains are restrictions set for the allowed domain names used + * for adding load balancer nodes. In order to submit a domain name as an address + * for the load balancer node to add, the user must verify that the domain is * valid by using the List Allowed Domains call. * * Note that this is actually a sub-resource of the load balancers service, * and not of the load balancer object. It's included here for convenience, * since it matches the pattern of the other LB subresources. */ -class AllowedDomain extends Readonly +class AllowedDomain extends ReadOnlyResource { - public $name; - + protected static $json_name = 'allowedDomain'; protected static $json_collection_name = 'allowedDomains'; protected static $json_collection_element = 'allowedDomain'; protected static $url_resource = 'loadbalancers/alloweddomains'; - } diff --git a/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/BillableLoadBalancer.php b/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/BillableLoadBalancer.php deleted file mode 100644 index 3be10962c..000000000 --- a/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/BillableLoadBalancer.php +++ /dev/null @@ -1,22 +0,0 @@ - - * @author Jamie Hannaford - */ - -namespace OpenCloud\LoadBalancer\Resource; - -/** - * Used to get a list of billable load balancers for a specific date range - */ -class BillableLoadBalancer extends Readonly -{ - - protected static $url_resource = 'loadbalancers/billable'; - protected static $json_name = null; -} diff --git a/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/CertificateMapping.php b/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/CertificateMapping.php new file mode 100644 index 000000000..3b770a212 --- /dev/null +++ b/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/CertificateMapping.php @@ -0,0 +1,96 @@ +certificateMapping = new \stdClass(); + foreach ($params as $name => $value) { + if (!in_array($name, $updateFields)) { + throw new InvalidArgumentError("You cannot update ${name}."); + } + $object->certificateMapping->$name = $this->$name; + } + + return $object; + } +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/ConnectionLogging.php b/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/ConnectionLogging.php index 21b0604ec..f371f1603 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/ConnectionLogging.php +++ b/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/ConnectionLogging.php @@ -1,39 +1,43 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\LoadBalancer\Resource; /** - * The connection logging feature allows logs to be delivered to a Cloud Files - * account every hour. For HTTP-based protocol traffic, these are Apache-style + * The connection logging feature allows logs to be delivered to a Cloud Files + * account every hour. For HTTP-based protocol traffic, these are Apache-style * access logs. For all other traffic, this is connection and transfer logging. */ -class ConnectionLogging extends SubResource +class ConnectionLogging extends AbstractResource { + public $enabled; - public $enabled; - protected static $json_name = "connectionLogging"; protected static $url_resource = "connectionlogging"; - + protected $createKeys = array('enabled'); - public function create($params = array()) - { - return $this->update($params); + public function create($params = array()) + { + return $this->update($params); } - public function delete() - { - return $this->noDelete(); + public function delete() + { + return $this->noDelete(); } - } diff --git a/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/ConnectionThrottle.php b/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/ConnectionThrottle.php index 0b488c347..5977ee716 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/ConnectionThrottle.php +++ b/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/ConnectionThrottle.php @@ -1,57 +1,63 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\LoadBalancer\Resource; /** - * The connection throttling feature imposes limits on the number of connections - * per IP address to help mitigate malicious or abusive traffic to your - * applications. The attributes in the table that follows can be configured + * The connection throttling feature imposes limits on the number of connections + * per IP address to help mitigate malicious or abusive traffic to your + * applications. The attributes in the table that follows can be configured * based on the traffic patterns for your sites. */ -class ConnectionThrottle extends SubResource +class ConnectionThrottle extends NonIdUriResource { /** - * Allow at least this number of connections per IP address before applying - * throttling restrictions. Setting a value of 0 allows unlimited + * Allow at least this number of connections per IP address before applying + * throttling restrictions. Setting a value of 0 allows unlimited * simultaneous connections; otherwise, set a value between 1 and 1000. - * + * * @var int */ public $minConnections; - + /** - * Maximum number of connections to allow for a single IP address. Setting a - * value of 0 will allow unlimited simultaneous connections; otherwise set a + * Maximum number of connections to allow for a single IP address. Setting a + * value of 0 will allow unlimited simultaneous connections; otherwise set a * value between 1 and 100000. - * - * @var int + * + * @var int */ public $maxConnections; - + /** - * Maximum number of connections allowed from a single IP address in the - * defined rateInterval. Setting a value of 0 allows an unlimited connection + * Maximum number of connections allowed from a single IP address in the + * defined rateInterval. Setting a value of 0 allows an unlimited connection * rate; otherwise, set a value between 1 and 100000. - * + * * @var int */ public $maxConnectionRate; - + /** - * Frequency (in seconds) at which the maxConnectionRate is assessed. - * For example, a maxConnectionRate of 30 with a rateInterval of 60 would - * allow a maximum of 30 connections per minute for a single IP address. + * Frequency (in seconds) at which the maxConnectionRate is assessed. + * For example, a maxConnectionRate of 30 with a rateInterval of 60 would + * allow a maximum of 30 connections per minute for a single IP address. * This value must be between 1 and 3600. - * + * * @var int */ public $rateInterval; @@ -66,12 +72,8 @@ class ConnectionThrottle extends SubResource 'rateInterval' ); - /** - * create uses PUT like Update - */ - public function create($params = array()) - { - return $this->update($params); + public function create($params = array()) + { + return $this->update($params); } - } diff --git a/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/ContentCaching.php b/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/ContentCaching.php index a96e2494e..0700cd57d 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/ContentCaching.php +++ b/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/ContentCaching.php @@ -1,47 +1,52 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\LoadBalancer\Resource; /** - * When content caching is enabled, recently-accessed files are stored on the - * load balancer for easy retrieval by web clients. Content caching improves the - * performance of high traffic web sites by temporarily storing data that was - * recently accessed. While it's cached, requests for that data will be served - * by the load balancer, which in turn reduces load off the back end nodes. The - * result is improved response times for those requests and less load on the web + * When content caching is enabled, recently-accessed files are stored on the + * load balancer for easy retrieval by web clients. Content caching improves the + * performance of high traffic web sites by temporarily storing data that was + * recently accessed. While it's cached, requests for that data will be served + * by the load balancer, which in turn reduces load off the back end nodes. The + * result is improved response times for those requests and less load on the web * server. - * + * * @todo Should this be a separate class, or a property of LoadBalancer? */ -class ContentCaching extends SubResource +class ContentCaching extends AbstractResource { /** - * @var bool + * @var bool */ - public $enabled; - + public $enabled; + protected static $json_name = "contentCaching"; protected static $url_resource = "contentcaching"; - + protected $createKeys = array('enabled'); - public function create($params = array()) - { - return $this->update($params); + public function create($params = array()) + { + return $this->update($params); } - public function delete() - { - return $this->noDelete(); + public function delete() + { + return $this->noDelete(); } - } diff --git a/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/ErrorPage.php b/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/ErrorPage.php index 34304b894..3ff47df93 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/ErrorPage.php +++ b/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/ErrorPage.php @@ -1,49 +1,45 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\LoadBalancer\Resource; /** - * An error page is the html file that is shown to the end user when an error - * in the service has been thrown. By default every virtual server is provided - * with the default error file. It is also possible to submit a custom error page - * via the Load Balancers API. Refer to Section 4.2.3, “Error Page Operations” + * An error page is the html file that is shown to the end user when an error + * in the service has been thrown. By default every virtual server is provided + * with the default error file. It is also possible to submit a custom error page + * via the Load Balancers API. Refer to Section 4.2.3, “Error Page Operations” * for details (http://docs.rackspace.com/loadbalancers/api/v1.0/clb-devguide/content/List_Errorpage-d1e2218.html). */ -class ErrorPage extends SubResource +class ErrorPage extends NonIdUriResource { /** * HTML content for the custom error page. Must be 65536 characters or less. - * - * @var string + * + * @var string */ public $content; - + protected static $json_name = 'errorpage'; protected static $url_resource = 'errorpage'; - + protected $createKeys = array('content'); - - /** - * creates a new error page - * - * This calls the Update() method, since it requires a PUT to create - * a new error page. A POST request is not supported, since the URL - * resource is already defined. - * - * @param array $params - */ - public function create($params = array()) - { - return $this->update($params); - } + public function create($params = array()) + { + return $this->update($params); + } } diff --git a/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/HealthMonitor.php b/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/HealthMonitor.php index 410702e7f..e6fa6e2c8 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/HealthMonitor.php +++ b/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/HealthMonitor.php @@ -1,101 +1,107 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\LoadBalancer\Resource; /** - * Active health monitoring is a technique that uses synthetic transactions - * executed at periodic intervals to determine the condition of a node. One of - * the advantages of active health monitoring is that it does not require active - * transactions to be processed by the load balancer to determine whether or not - * a node is suitable for handling traffic. Active health monitoring is not + * Active health monitoring is a technique that uses synthetic transactions + * executed at periodic intervals to determine the condition of a node. One of + * the advantages of active health monitoring is that it does not require active + * transactions to be processed by the load balancer to determine whether or not + * a node is suitable for handling traffic. Active health monitoring is not * applied by default and must be enabled per load balancer. - * + * * The active health monitor can use one of three types of probes: - * + * * * connect * * HTTP * * HTTPS - * - * These probes are executed at configured intervals; in the event of a failure, - * the node status changes to OFFLINE and the node will not receive traffic. If, - * after running a subsequent test, the probe detects that the node has recovered, + * + * These probes are executed at configured intervals; in the event of a failure, + * the node status changes to OFFLINE and the node will not receive traffic. If, + * after running a subsequent test, the probe detects that the node has recovered, * then the node's status is changed to ONLINE and it is capable of servicing requests. */ -class HealthMonitor extends SubResource +class HealthMonitor extends NonIdUriResource { - /** * Type of the health monitor. Can either be "connect", "HTTP" or "HTTPS" - * - * @var string + * + * @var string */ public $type; - + /** - * The minimum number of seconds to wait before executing the health monitor. + * The minimum number of seconds to wait before executing the health monitor. * Must be a number between 1 and 3600. - * - * @var int + * + * @var int */ public $delay; - + /** - * Maximum number of seconds to wait for a connection to be established + * Maximum number of seconds to wait for a connection to be established * before timing out. Must be a number between 1 and 300. - * - * @var int + * + * @var int */ public $timeout; - + /** - * Number of permissible monitor failures before removing a node from rotation. + * Number of permissible monitor failures before removing a node from rotation. * Must be a number between 1 and 10. - * - * @var int + * + * @var int */ public $attemptsBeforeDeactivation; - + /** - * A regular expression that will be used to evaluate the contents of the + * A regular expression that will be used to evaluate the contents of the * body of the response. - * - * @var string + * + * @var string */ public $bodyRegex; - + /** * The name of a host for which the health monitors will check. - * - * @var string + * + * @var string */ public $hostHeader; - + /** * The HTTP path that will be used in the sample request. - * - * @var string + * + * @var string */ public $path; - + /** - * A regular expression that will be used to evaluate the HTTP status code + * A regular expression that will be used to evaluate the HTTP status code * returned in the response. - * - * @var string + * + * @var string */ public $statusRegex; - + protected static $json_name = 'healthMonitor'; protected static $url_resource = 'healthmonitor'; + protected $createKeys = array( 'type', 'delay', @@ -107,18 +113,8 @@ class HealthMonitor extends SubResource 'statusRegex' ); - /** - * creates a new health monitor - * - * This calls the Update() method, since it requires a PUT to create - * a new error page. A POST request is not supported, since the URL - * resource is already defined. - * - * @param array $params array of parameters - */ - public function create($params = array()) - { - return $this->update($params); + public function create($params = array()) + { + return $this->update($params); } - } diff --git a/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/LoadBalancer.php b/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/LoadBalancer.php index f059fde2e..551198c1b 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/LoadBalancer.php +++ b/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/LoadBalancer.php @@ -1,38 +1,44 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\LoadBalancer\Resource; -use OpenCloud\Common\PersistentObject; -use OpenCloud\Common\Lang; use OpenCloud\Common\Exceptions; -use OpenCloud\Common\Http\Message\Formatter; +use OpenCloud\Common\Log\Logger; +use OpenCloud\Common\Resource\PersistentResource; +use OpenCloud\DNS\Resource\HasPtrRecordsInterface; +use OpenCloud\LoadBalancer\Enum\NodeCondition; +use OpenCloud\LoadBalancer\Enum\IpType; +use OpenCloud\LoadBalancer\Enum\NodeType; /** - * A load balancer is a logical device which belongs to a cloud account. It is - * used to distribute workloads between multiple back-end systems or services, + * A load balancer is a logical device which belongs to a cloud account. It is + * used to distribute workloads between multiple back-end systems or services, * based on the criteria defined as part of its configuration. - * - * */ -class LoadBalancer extends PersistentObject +class LoadBalancer extends PersistentResource implements HasPtrRecordsInterface { - public $id; - + /** - * Name of the load balancer to create. The name must be 128 characters or + * Name of the load balancer to create. The name must be 128 characters or * less in length, and all UTF-8 characters are valid. - * - * @var string + * + * @var string */ public $name; @@ -42,62 +48,70 @@ class LoadBalancer extends PersistentObject * @var string */ public $port; - + /** * Protocol of the service which is being load balanced. - * - * @var string + * + * @var string */ public $protocol; - + /** * Type of virtual IP to add along with the creation of a load balancer. - * + * * @var array|Collection */ public $virtualIps = array(); - + /** * Nodes to be added to the load balancer. - * + * * @var array|Collection */ public $nodes = array(); - + /** - * The access list management feature allows fine-grained network access + * The access list management feature allows fine-grained network access * controls to be applied to the load balancer's virtual IP address. - * - * @var Collection + * + * @var Collection */ public $accessList; - + /** * Algorithm that defines how traffic should be directed between back-end nodes. * * @var Algorithm */ public $algorithm; - + + + /** + * Enables or disables HTTP to HTTPS redirection for the load balancer. + * + * @var bool + */ + public $httpsRedirect; + /** * Current connection logging configuration. - * + * * @var ConnectionLogging */ public $connectionLogging; - + /** - * Specifies limits on the number of connections per IP address to help + * Specifies limits on the number of connections per IP address to help * mitigate malicious or abusive traffic to your applications. - * + * * @var ConnectionThrottle */ public $connectionThrottle; - + /** - * The type of health monitor check to perform to ensure that the service is + * The type of health monitor check to perform to ensure that the service is * performing properly. - * + * * @var HealthMonitor */ public $healthMonitor; @@ -109,23 +123,23 @@ class LoadBalancer extends PersistentObject * @var SessionPersistance */ public $sessionPersistence; - + /** - * Information (metadata) that can be associated with each load balancer for + * Information (metadata) that can be associated with each load balancer for * the client's personal use. - * - * @var array|Metadata + * + * @var array|Metadata */ public $metadata = array(); - + /** - * The timeout value for the load balancer and communications with its nodes. + * The timeout value for the load balancer and communications with its nodes. * Defaults to 30 seconds with a maximum of 120 seconds. - * - * @var int + * + * @var int */ public $timeout; - + public $created; public $updated; public $status; @@ -137,6 +151,7 @@ class LoadBalancer extends PersistentObject protected static $url_resource = 'loadbalancers'; protected $associatedResources = array( + 'certificateMapping' => 'CertificateMapping', 'node' => 'Node', 'virtualIp' => 'VirtualIp', 'connectionLogging' => 'ConnectionLogging', @@ -145,12 +160,13 @@ class LoadBalancer extends PersistentObject ); protected $associatedCollections = array( - //'nodes' => 'Node', - 'virtualIps' => 'VirtualIp', - 'accessList' => 'Access' + 'certificateMappings' => 'CertificateMapping', + 'nodes' => 'Node', + 'virtualIps' => 'VirtualIp', + 'accessList' => 'Access' ); - private $createKeys = array( + protected $createKeys = array( 'name', 'port', 'protocol', @@ -161,106 +177,105 @@ class LoadBalancer extends PersistentObject 'connectionLogging', 'connectionThrottle', 'healthMonitor', - 'sessionPersistence' + 'sessionPersistence', + 'httpsRedirect' ); /** - * adds a node to the load balancer - * * This method creates a Node object and adds it to a list of Nodes - * to be added to the LoadBalancer. *Very important:* this method *NEVER* - * adds the nodes directly to the load balancer itself; it stores them - * on the object, and the nodes are added later, in one of two ways: + * to be added to the LoadBalancer. This method will not add the nodes + * directly to the load balancer itself; it stores them in an array and + * the nodes are added later, in one of two ways: * - * * for a new LoadBalancer, the Nodes are added as part of the Create() - * method call. - * * for an existing LoadBalancer, you must call the AddNodes() method + * * for a new load balancer, the nodes are added as part of the create() method call + * * for an existing load balancer, you must call the addNodes() method * - * @api - * @param string $address the IP address of the node - * @param integer $port the port # of the node + * @param string $address the IP address of the node + * @param integer $port the port # of the node * @param boolean $condition the initial condition of the node - * @param string $type either PRIMARY or SECONDARY - * @param integer $weight the node weight (for round-robin) - * @throws \OpenCloud\DomainError if value is not valid + * @param string $type either PRIMARY or SECONDARY + * @param integer $weight the node weight (for round-robin) + * + * @throws \InvalidArgumentException * @return void */ public function addNode( - $address, - $port, - $condition = 'ENABLED', - $type = null, + $address, + $port, + $condition = NodeCondition::ENABLED, + $type = null, $weight = null ) { - $node = $this->Node(); - $node->address = $address; - $node->port = $port; - $cond = strtoupper($condition); - - switch($cond) { - case 'ENABLED': - case 'DISABLED': - case 'DRAINING': - $node->condition = $cond; - break; - default: - throw new Exceptions\DomainError(sprintf( - Lang::translate('Value [%s] for Node::condition is not valid'), - $condition - )); + $allowedConditions = array( + NodeCondition::ENABLED, + NodeCondition::DISABLED, + NodeCondition::DRAINING + ); + + if (!in_array($condition, $allowedConditions)) { + throw new \InvalidArgumentException(sprintf( + "Invalid condition. It must one of the following: %s", + implode(', ', $allowedConditions) + )); } - if ($type !== null) { - switch(strtoupper($type)) { - case 'PRIMARY': - case 'SECONDARY': - $node->type = $type; - break; - default: - throw new Exceptions\DomainError(sprintf( - Lang::translate('Value [%s] for Node::type is not valid'), - $type - )); - } + $allowedTypes = array(NodeType::PRIMARY, NodeType::SECONDARY); + if ($type && !in_array($type, $allowedTypes)) { + throw new \InvalidArgumentException(sprintf( + "Invalid type. It must one of the following: %s", + implode(', ', $allowedTypes) + )); } - if ($weight !== null) { - if (is_integer($weight)) { - $node->weight = $weight; - } else { - throw new Exceptions\DomainError(sprintf( - Lang::translate('Value [%s] for Node::weight must be integer'), - $weight - )); - } + if ($weight && !is_numeric($weight)) { + throw new \InvalidArgumentException('Invalid weight. You must supply a numeric type'); } // queue it - $this->nodes[] = $node; + $this->nodes[] = $this->node(array( + 'address' => $address, + 'port' => $port, + 'condition' => $condition, + 'type' => $type, + 'weight' => $weight + )); } - - public function addNodes() + + /** + * Creates currently added nodes by sending them to the API + * + * @return array of {@see \Guzzle\Http\Message\Response} objects + * @throws \OpenCloud\Common\Exceptions\MissingValueError + */ + public function addNodes() { - if (count($this->nodes) < 1) { + if (empty($this->nodes)) { throw new Exceptions\MissingValueError( - Lang::translate('Cannot add nodes; no nodes are defined') + 'Cannot add nodes; no nodes are defined' ); } - // iterate through all the nodes - foreach($this->nodes as $node) { - $resp = $node->Create(); + $requestData = array('nodes' => array()); + + /** @var Node $node */ + foreach ($this->nodes as $node) { + // Only add the node if it is new + if (null === $node->getId()) { + $nodeJson = $node->createJson(); + $requestData['nodes'][] = $nodeJson['nodes'][0]; + } } - return $resp; + $request = $this->getClient()->post($node->getUrl(), self::getJsonHeader(), json_encode($requestData)); + + return $this->getClient()->send($request); } - + /** * Remove a node from this load-balancer * - * @api * @param int $id id of the node - * @return mixed + * @return \Guzzle\Http\Message\Response */ public function removeNode($nodeId) { @@ -268,28 +283,22 @@ public function removeNode($nodeId) } /** - * adds a virtual IP to the load balancer + * Adds a virtual IP to the load balancer. You can use the strings 'PUBLIC' + * or 'SERVICENET' to indicate the public or internal networks, or you can + * pass the `Id` of an existing IP address. * - * You can use the strings `'PUBLIC'` or `'SERVICENET`' to indicate the - * public or internal networks, or you can pass the `Id` of an existing - * IP address. - * - * @api - * @param string $id either 'public' or 'servicenet' or an ID of an - * existing IP address + * @param string $id either 'public' or 'servicenet' or an ID of an + * existing IP address * @param integer $ipVersion either null, 4, or 6 (both, IPv4, or IPv6) * @return void */ - public function addVirtualIp($type = 'PUBLIC', $ipVersion = NULL) + public function addVirtualIp($type = IpType::PUBLIC_TYPE, $ipVersion = null) { $object = new \stdClass(); - /** - * check for PUBLIC or SERVICENET - */ - switch(strtoupper($type)) { - case 'PUBLIC': - case 'SERVICENET': + switch (strtoupper($type)) { + case IpType::PUBLIC_TYPE: + case IpType::SERVICENET_TYPE: $object->type = strtoupper($type); break; default: @@ -298,16 +307,16 @@ public function addVirtualIp($type = 'PUBLIC', $ipVersion = NULL) } if ($ipVersion) { - switch($ipVersion) { + switch ($ipVersion) { case 4: - $object->version = 'IPV4'; + $object->ipVersion = IpType::IPv4; break; case 6: - $object->version = 'IPV6'; + $object->ipVersion = IpType::IPv6; break; default: throw new Exceptions\DomainError(sprintf( - Lang::translate('Value [%s] for ipVersion is not valid'), + 'Value [%s] for ipVersion is not valid', $ipVersion )); } @@ -321,7 +330,7 @@ public function addVirtualIp($type = 'PUBLIC', $ipVersion = NULL) if ($this->Id()) { $virtualIp = $this->virtualIp(); $virtualIp->type = $type; - $virtualIp->ipVersion = $object->version; + $virtualIp->ipVersion = $object->ipVersion; return $virtualIp->create(); } else { // queue it @@ -332,193 +341,367 @@ public function addVirtualIp($type = 'PUBLIC', $ipVersion = NULL) } /** - * returns a Node object + * Returns a Node + * + * @return \OpenCloud\LoadBalancer\Resource\Node */ - public function node($id = null) + public function node($id = null) { - $resource = new Node($this->getService()); - $resource->setParent($this)->populate($id); - return $resource; + return $this->getService()->resource('Node', $id, $this); } /** * returns a Collection of Nodes + * + * @return \OpenCloud\Common\Collection\PaginatedIterator */ - public function nodeList() + public function nodeList() { return $this->getService()->resourceList('Node', null, $this); } /** - * returns a NodeEvent object + * Returns a NodeEvent object + * + * @return \OpenCloud\LoadBalancer\Resource\NodeEvent */ - public function nodeEvent() + public function nodeEvent() { - $resource = new NodeEvent($this->getService()); - $resource->setParent($this)->initialRefresh(); - return $resource; + return $this->getService()->resource('NodeEvent', null, $this); } /** - * returns a Collection of NodeEvents + * Returns a Collection of NodeEvents + * + * @return \OpenCloud\Common\Collection\PaginatedIterator */ - public function nodeEventList() + public function nodeEventList() { - return $this->getParent()->resourceList('NodeEvent', null, $this); + return $this->getService()->resourceList('NodeEvent', null, $this); } /** - * returns a single Virtual IP (not called publicly) + * Returns a single Virtual IP (not called publicly) + * + * @return \OpenCloud\LoadBalancer\Resource\VirtualIp */ - public function virtualIp($data = null) + public function virtualIp($data = null) { - $resource = new VirtualIp($this->getService(), $data); - $resource->setParent($this)->initialRefresh(); - return $resource; - + return $this->getService()->resource('VirtualIp', $data, $this); } /** - * returns a Collection of Virtual Ips + * @return \OpenCloud\Common\Collection\PaginatedIterator */ - public function virtualIpList() + public function virtualIpList() { return $this->getService()->resourceList('VirtualIp', null, $this); } /** + * Returns a Certificate Mapping. + * + * @param int|array $id (Optional) Either a particular Certificate mapping ID, or an array of data about the + * mapping. An array can include these keys: hostName, privateKey, certificate, + * intermediateCertificate. + * @return \OpenCloud\LoadBalancer\Resource\CertificateMapping */ - public function sessionPersistence() + public function certificateMapping($id = null) { - $resource = new SessionPersistence($this->getService()); - $resource->setParent($this)->initialRefresh(); - return $resource; + return $this->getService()->resource('CertificateMapping', $id, $this); } /** - * returns the load balancer's error page object + * Returns a Collection of Certificate Mappings. * - * @api - * @return ErrorPage + * @return \OpenCloud\Common\Collection\PaginatedIterator */ - public function errorPage() + public function certificateMappingList() { - $resource = new ErrorPage($this->getService()); - $resource->setParent($this)->initialRefresh(); - return $resource; + return $this->getService()->resourceList('CertificateMapping', null, $this); } /** - * returns the load balancer's health monitor object + * Creates a certificate mapping. * - * @api - * @return HealthMonitor + * @throws \OpenCloud\Common\Exceptions\MissingValueError + * + * @param string $hostName The domain name for the certificate. + * @param string $privateKey The private key for the certificate + * @param string $certificate The certificate itself. + * @param string $intermediateCertificate The intermediate certificate chain. + * @return array An array of \Guzzle\Http\Message\Response objects. + */ + public function addCertificateMapping( + $hostName, + $privateKey, + $certificate, + $intermediateCertificate = null + ) { + $certificateMapping = $this->certificateMapping( + array( + 'hostName' => $hostName, + 'privateKey' => $privateKey, + 'certificate' => $certificate, + 'intermediateCertificate' => $intermediateCertificate + ) + ); + $json = json_encode($certificateMapping->createJson()); + $request = $this->getClient()->post($certificateMapping->getUrl(), self::getJsonHeader(), $json); + + return $this->getClient()->send($request); + } + + /** + * Updates a certificate mapping. + * + * @param int $id ID of the certificate mapping. + * @param string $hostName (Optional) The domain name of the certificate. + * @param string $privateKey (Optional) The private key for the certificate. + * @param string $certificate The certificate itself. + * @param string $intermediateCertificate The intermediate certificate chain. + * @return array An array of \Guzzle\Http\Message\Response objects. + */ + public function updateCertificateMapping( + $id, + $hostName = null, + $privateKey = null, + $certificate = null, + $intermediateCertificate = null + ) { + $certificateMapping = $this->certificateMapping($id); + return $certificateMapping->update( + array( + 'hostName' => $hostName, + 'privateKey' => $privateKey, + 'certificate' => $certificate, + 'intermediateCertificate' => $intermediateCertificate + ) + ); + } + + /** + * Remove a certificate mapping. + * + * @param int $id ID of the certificate mapping. + * @return \Guzzle\Http\Message\Response */ - public function healthMonitor() + public function removeCertificateMapping($id) { - $resource = new HealthMonitor($this->getService()); - $resource->setParent($this)->initialRefresh(); - return $resource; + return $this->certificateMapping($id)->delete(); } /** - * returns statistics on the load balancer operation + * Return the session persistence resource * - * cannot be created, updated, or deleted + * @return \OpenCloud\LoadBalancer\Resource\SessionPersistence + */ + public function sessionPersistence() + { + return $this->getService()->resource('SessionPersistence', null, $this); + } + + /** + * Returns the load balancer's error page object * - * @api - * @return Stats + * @return \OpenCloud\LoadBalancer\Resource\ErrorPage + */ + public function errorPage() + { + return $this->getService()->resource('ErrorPage', null, $this); + } + + /** + * Returns the load balancer's health monitor object + * + * @return \OpenCloud\LoadBalancer\Resource\HealthMonitor + */ + public function healthMonitor() + { + return $this->getService()->resource('HealthMonitor', null, $this); + } + + /** + * Returns statistics on the load balancer operation + * + * @return \OpenCloud\LoadBalancer\Resource\Stats + */ + public function stats() + { + return $this->getService()->resource('Stats', null, $this); + } + + /** + * @return \OpenCloud\Common\Collection\PaginatedIterator */ - public function stats() + public function usage() { - $resource = new Stats($this->getService()); - $resource->setParent($this)->initialRefresh(); - return $resource; + return $this->getService()->resourceList('UsageRecord', null, $this); } /** + * Return an access resource + * + * @return \OpenCloud\LoadBalancer\Resource\Access */ - public function usage() + public function access($data = null) { - $resource = new Usage($this->getService()); - $resource->setParent($this)->initialRefresh(); - return $resource; + return $this->getService()->resource('Access', $data, $this); } /** + * Creates an access list. You must provide an array of \stdClass objects, + * each of which contains `type' and `address' properties. Valid types for + * the former are: "DENY" or "ALLOW". The address must be a valid IP + * address, either v4 or v6. + * + * @param stdClass[] $list + * + * @return \Guzzle\Http\Message\Response */ - public function access($data = null) + public function createAccessList(array $list) { - $resource = new Access($this->getService(), $data); - $resource->setParent($this)->initialRefresh(); - return $resource; + $url = $this->getUrl(); + $url->addPath('accesslist'); + + $json = json_encode($list); + $this->checkJsonError(); + + return $this->getClient()->post($url, self::getJsonHeader(), $json)->send(); } /** + * @return \OpenCloud\Common\Collection\PaginatedIterator */ - public function accessList() + public function accessList() { return $this->getService()->resourceList('Access', null, $this); } /** + * Return a connection throttle resource + * + * @return \OpenCloud\LoadBalancer\Resource\ConnectionThrottle + */ + public function connectionThrottle() + { + return $this->getService()->resource('ConnectionThrottle', null, $this); + } + + /** + * Find out whether connection logging is enabled for this load balancer + * + * @return bool Returns TRUE if enabled, FALSE if not + */ + public function hasConnectionLogging() + { + $url = clone $this->getUrl(); + $url->addPath('connectionlogging'); + + $response = $this->getClient()->get($url)->send()->json(); + + return isset($response['connectionLogging']['enabled']) + && $response['connectionLogging']['enabled'] === true; + } + + /** + * Set the connection logging setting for this load balancer + * + * @param $bool Set to TRUE to enable, FALSE to disable + * @return \Guzzle\Http\Message\Response */ - public function connectionThrottle() + public function enableConnectionLogging($bool) { - $resource = new ConnectionThrottle($this->getService()); - $resource->setParent($this)->initialRefresh(); - return $resource; + $url = clone $this->getUrl(); + $url->addPath('connectionlogging'); + + $body = array('connectionLogging' => (bool) $bool); + + return $this->getClient()->put($url, self::getJsonHeader(), $body)->send(); } /** + * @deprecated */ - public function connectionLogging() + public function connectionLogging() { - $resource = new ConnectionLogging($this->getService()); - $resource->setParent($this)->initialRefresh(); - return $resource; + $this->getLogger()->warning(Logger::deprecated(__METHOD__, 'hasConnectionLogging or enableConnectionLogging')); } /** + * Find out whether content caching is enabled for this load balancer + * + * @return bool Returns TRUE if enabled, FALSE if not */ - public function contentCaching() + public function hasContentCaching() { - $resource = new ContentCaching($this->getService()); - $resource->setParent($this)->initialRefresh(); - return $resource; + $url = clone $this->getUrl(); + $url->addPath('contentcaching'); + + $response = $this->getClient()->get($url)->send()->json(); + + return isset($response['contentCaching']['enabled']) + && $response['contentCaching']['enabled'] === true; } /** + * Set the content caching setting for this load balancer + * + * @param $bool Set to TRUE to enable, FALSE to disable + * @return \Guzzle\Http\Message\Response */ - public function SSLTermination() + public function enableContentCaching($bool) { - $resource = new SSLTermination($this->getService()); - $resource->setParent($this)->initialRefresh(); - return $resource; + $url = clone $this->getUrl(); + $url->addPath('contentcaching'); + + $body = array('contentCaching' => array('enabled' => (bool) $bool)); + $body = json_encode($body); + $this->checkJsonError(); + + return $this->getClient()->put($url, self::getJsonHeader(), $body)->send(); } /** + * @deprecated */ - public function metadata($data = null) + public function contentCaching() { - $resource = new Metadata($this->getService(), $data); - $resource->setParent($this)->initialRefresh(); - return $resource; + $this->getLogger()->warning(sprintf( + 'The %s method is deprecated, please use %s instead', __METHOD__, 'hasContentCaching or setContentCaching')); } /** + * Return a SSL Termination resource + * + * @return \OpenCloud\LoadBalancer\Resource\SSLTermination */ - public function metadataList() + public function SSLTermination() { - return $this->getService()->resourceList('Metadata', null, $this); + return $this->getService()->resource('SSLTermination', null, $this); } /** - * returns the JSON object for Create() + * Return a metadata item * - * @return stdClass + * @return \OpenCloud\LoadBalancer\Resource\Metadata */ - protected function createJson() + public function metadata($data = null) + { + return $this->getService()->resource('Metadata', $data, $this); + } + + /** + * Return a collection of metadata items + * + * @return \OpenCloud\Common\Collection\PaginatedIterator + */ + public function metadataList() + { + return $this->getService()->resourceList('Metadata', null, $this); + } + + protected function createJson() { $element = (object) array(); @@ -533,34 +716,26 @@ protected function createJson() } $element->nodes[] = (object) $nodeObject; } - } elseif($key == 'virtualIps') { - foreach ($this->virtualIps as $virtualIp) { + } elseif ($key == 'virtualIps') { + foreach ($this->virtualIps as $virtualIp) { $element->virtualIps[] = $virtualIp; } } elseif (isset($this->$key)) { $element->$key = $this->$key; } } - + $object = (object) array($this->jsonName() => $element); return $object; } - /** - * returns the JSON object for Update() - * - * @return stdClass - * @throws \OpenCloud\Common\Exceptions\InvalidParameterError - */ protected function updateJson($params = array()) { + $updatableFields = array('name', 'algorithm', 'protocol', 'port', 'timeout', 'halfClosed', 'httpsRedirect'); - $updatableFields = array('name','algorithm','protocol','port','timeout','halfClosed'); - - //Validate supplied fields $fields = array_keys($params); - foreach($fields as $field) { + foreach ($fields as $field) { if (!in_array($field, $updatableFields)) { throw new Exceptions\InvalidArgumentError("You cannot update $field."); } @@ -568,9 +743,10 @@ protected function updateJson($params = array()) $object = new \stdClass(); $object->loadBalancer = new \stdClass(); - foreach($params as $name => $value) { + foreach ($params as $name => $value) { $object->loadBalancer->$name = $this->$name; } + return $object; - } + } } diff --git a/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/Metadata.php b/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/Metadata.php index f9fc7b329..c9543cbd9 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/Metadata.php +++ b/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/Metadata.php @@ -1,12 +1,18 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\LoadBalancer\Resource; @@ -14,9 +20,8 @@ /** * Sub-resource to manage Metadata */ -class Metadata extends SubResource +class Metadata extends AbstractResource { - public $id; public $key; public $value; @@ -26,13 +31,12 @@ class Metadata extends SubResource protected static $url_resource = 'metadata'; protected $createKeys = array( - 'key', + 'key', 'value' ); - public function name() + public function name() { return $this->key; } - } diff --git a/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/Node.php b/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/Node.php index 98ac798c2..a5e73ae77 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/Node.php +++ b/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/Node.php @@ -1,109 +1,114 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\LoadBalancer\Resource; -use OpenCloud\Common\PersistentObject; +use OpenCloud\Common\Resource\PersistentResource; /** - * The nodes defined by the load balancer are responsible for servicing the - * requests received through the load balancer's virtual IP. By default, the - * load balancer employs a basic health check that ensures the node is listening - * on its defined port. The node is checked at the time of addition and at regular - * intervals as defined by the load balancer health check configuration. If a - * back-end node is not listening on its port or does not meet the conditions of - * the defined active health check for the load balancer, then the load balancer - * will not forward connections and its status will be listed as "OFFLINE". Only - * nodes that are in an "ONLINE" status will receive and be able to service + * The nodes defined by the load balancer are responsible for servicing the + * requests received through the load balancer's virtual IP. By default, the + * load balancer employs a basic health check that ensures the node is listening + * on its defined port. The node is checked at the time of addition and at regular + * intervals as defined by the load balancer health check configuration. If a + * back-end node is not listening on its port or does not meet the conditions of + * the defined active health check for the load balancer, then the load balancer + * will not forward connections and its status will be listed as "OFFLINE". Only + * nodes that are in an "ONLINE" status will receive and be able to service * traffic from the load balancer. - * - * All nodes have an associated status that indicates whether the node is - * ONLINE, OFFLINE, or DRAINING. Only nodes that are in ONLINE status will - * receive and be able to service traffic from the load balancer. The OFFLINE - * status represents a node that cannot accept or service traffic. A node in - * DRAINING status represents a node that stops the traffic manager from sending - * any additional new connections to the node, but honors established sessions. - * If the traffic manager receives a request and session persistence requires - * that the node is used, the traffic manager will use it. The status is + * + * All nodes have an associated status that indicates whether the node is + * ONLINE, OFFLINE, or DRAINING. Only nodes that are in ONLINE status will + * receive and be able to service traffic from the load balancer. The OFFLINE + * status represents a node that cannot accept or service traffic. A node in + * DRAINING status represents a node that stops the traffic manager from sending + * any additional new connections to the node, but honors established sessions. + * If the traffic manager receives a request and session persistence requires + * that the node is used, the traffic manager will use it. The status is * determined by the passive or active health monitors. - * - * If the WEIGHTED_ROUND_ROBIN load balancer algorithm mode is selected, then - * the caller should assign the relevant weights to the node as part of the - * weight attribute of the node element. When the algorithm of the load balancer - * is changed to WEIGHTED_ROUND_ROBIN and the nodes do not already have an + * + * If the WEIGHTED_ROUND_ROBIN load balancer algorithm mode is selected, then + * the caller should assign the relevant weights to the node as part of the + * weight attribute of the node element. When the algorithm of the load balancer + * is changed to WEIGHTED_ROUND_ROBIN and the nodes do not already have an * assigned weight, the service will automatically set the weight to "1" for all nodes. - * - * One or more secondary nodes can be added to a specified load balancer so that - * if all the primary nodes fail, traffic can be redirected to secondary nodes. + * + * One or more secondary nodes can be added to a specified load balancer so that + * if all the primary nodes fail, traffic can be redirected to secondary nodes. * The type attribute allows configuring the node as either PRIMARY or SECONDARY. */ -class Node extends PersistentObject +class Node extends PersistentResource { - public $id; - + /** * IP address or domain name for the node. - * + * * @var string */ public $address; - + /** * Port number for the service you are load balancing. - * - * @var int + * + * @var int */ public $port; - + /** * Condition for the node, which determines its role within the load balancer. - * - * @var string + * + * @var string */ public $condition; - + /** * Current state of the node. Can either be ONLINE, OFFLINE or DRAINING. - * - * @var string + * + * @var string */ public $status; - + /** - * Weight of node to add. If the WEIGHTED_ROUND_ROBIN load balancer algorithm - * mode is selected, then the user should assign the relevant weight to the + * Weight of node to add. If the WEIGHTED_ROUND_ROBIN load balancer algorithm + * mode is selected, then the user should assign the relevant weight to the * node using the weight attribute for the node. Must be an integer from 1 to 100. - * - * @var int + * + * @var int */ public $weight; - + /** - * Type of node to add: - * - * * PRIMARY: Nodes defined as PRIMARY are in the normal rotation to receive + * Type of node to add: + * + * * PRIMARY: Nodes defined as PRIMARY are in the normal rotation to receive * traffic from the load balancer. - * - * * SECONDARY: Nodes defined as SECONDARY are only in the rotation to + * + * * SECONDARY: Nodes defined as SECONDARY are only in the rotation to * receive traffic from the load balancer when all the primary nodes fail. - * - * @var string + * + * @var string */ public $type; - protected static $json_name = FALSE; + protected static $json_name = false; protected static $json_collection_name = 'nodes'; protected static $url_resource = 'nodes'; - + public $createKeys = array( 'address', 'port', @@ -117,19 +122,20 @@ class Node extends PersistentObject * * @return string */ - public function name() + public function name() { return get_class() . '[' . $this->Id() . ']'; } - protected function createJson() + public function createJson() { - $nodes = (object) array('node' => new \stdClass); - foreach($this->createKeys as $key) { - $nodes->node->$key = $this->$key; + $nodes = array('node' => array()); + + foreach ($this->createKeys as $key) { + $nodes['node'][$key] = $this->$key; } - - return (object) array('nodes' => array($nodes)); + + return array('nodes' => array($nodes)); } protected function updateJson($params = array()) @@ -148,28 +154,22 @@ protected function updateJson($params = array()) } /** - * factory method to create a new Metadata child of the Node + * Returns a Metadata item * - * @api * @return Metadata */ - public function metadata($data = null) + public function metadata($data = null) { - return new Metadata($this, $data); + return $this->getService()->resource('Metadata', $data, $this); } /** - * factory method to create a Collection of Metadata object - * - * Note that these are metadata children of the Node, not of the - * LoadBalancer. + * Returns a paginated collection of metadata * - * @api - * @return Collection of Metadata + * @return PaginatedIterator */ - public function metadataList() + public function metadataList() { - return $this->getService()->collection('OpenCloud\LoadBalancer\Resource\Metadata', null, $this); + return $this->getService()->resourceList('Metadata', null, $this); } - } diff --git a/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/NodeEvent.php b/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/NodeEvent.php index 2cca015e9..a440ed5fc 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/NodeEvent.php +++ b/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/NodeEvent.php @@ -1,23 +1,28 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\LoadBalancer\Resource; /** - * This class will retrieve a list of events associated with the activity + * This class will retrieve a list of events associated with the activity * between the node and the load balancer. The events report errors found with the node. */ -class NodeEvent extends Readonly +class NodeEvent extends ReadOnlyResource { - public $detailedMessage; public $nodeId; public $id; @@ -34,5 +39,4 @@ class NodeEvent extends Readonly protected static $json_name = 'nodeServiceEvent'; protected static $url_resource = 'nodes/events'; - } diff --git a/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/NonIdUriResource.php b/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/NonIdUriResource.php new file mode 100644 index 000000000..83e1ccddf --- /dev/null +++ b/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/NonIdUriResource.php @@ -0,0 +1,32 @@ +refreshFromParent(); + } +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/Protocol.php b/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/Protocol.php index fbb876107..509b66212 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/Protocol.php +++ b/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/Protocol.php @@ -1,30 +1,35 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\LoadBalancer\Resource; /** - * All load balancers must define the protocol of the service which is being - * load balanced. The protocol selection should be based on the protocol of the - * back-end nodes. When configuring a load balancer, the default port for the + * All load balancers must define the protocol of the service which is being + * load balanced. The protocol selection should be based on the protocol of the + * back-end nodes. When configuring a load balancer, the default port for the * given protocol will be selected unless otherwise specified. - * + * * @link http://docs.rackspace.com/loadbalancers/api/v1.0/clb-devguide/content/List_Load_Balancing_Protocols-d1e4269.html */ -class Protocol extends Readonly +class Protocol extends ReadOnlyResource { - public $name; public $port; + protected static $json_name = 'protocol'; protected static $url_resource = 'loadbalancers/protocols'; - } diff --git a/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/ReadOnlyResource.php b/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/ReadOnlyResource.php new file mode 100644 index 000000000..1b18531ab --- /dev/null +++ b/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/ReadOnlyResource.php @@ -0,0 +1,42 @@ +noCreate(); + } + + public function update($params = array()) + { + return $this->noUpdate(); + } + + public function delete() + { + return $this->noDelete(); + } +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/Readonly.php b/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/Readonly.php deleted file mode 100644 index 29586de27..000000000 --- a/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/Readonly.php +++ /dev/null @@ -1,37 +0,0 @@ - - * @author Jamie Hannaford - */ - -namespace OpenCloud\LoadBalancer\Resource; - -/** - * This defines a read-only SubResource - one that cannot be created, updated, - * or deleted. Many subresources are like this, and this simplifies their - * class definitions. - */ -abstract class Readonly extends SubResource -{ - - public function create($params = array()) - { - return $this->noCreate(); - } - - public function update($params = array()) - { - return $this->noUpdate(); - } - - public function delete() - { - return $this->noDelete(); - } - -} diff --git a/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/SSLTermination.php b/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/SSLTermination.php index f8535bc06..1b0367d05 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/SSLTermination.php +++ b/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/SSLTermination.php @@ -1,82 +1,88 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\LoadBalancer\Resource; /** - * The SSL Termination feature allows a load balancer user to terminate SSL - * traffic at the load balancer layer versus at the web server layer. A user may - * choose to configure SSL Termination using a key and an SSL certificate or an + * The SSL Termination feature allows a load balancer user to terminate SSL + * traffic at the load balancer layer versus at the web server layer. A user may + * choose to configure SSL Termination using a key and an SSL certificate or an * (Intermediate) SSL certificate. - * - * When SSL Termination is configured on a load balancer, a secure shadow server - * is created that listens only for secure traffic on a user-specified port. - * This shadow server is only visible to and manageable by the system. Existing - * or updated attributes on a load balancer with SSL Termination will also apply - * to its shadow server. For example, if Connection Logging is enabled on an SSL - * load balancer, it will also be enabled on the shadow server and Cloud Files + * + * When SSL Termination is configured on a load balancer, a secure shadow server + * is created that listens only for secure traffic on a user-specified port. + * This shadow server is only visible to and manageable by the system. Existing + * or updated attributes on a load balancer with SSL Termination will also apply + * to its shadow server. For example, if Connection Logging is enabled on an SSL + * load balancer, it will also be enabled on the shadow server and Cloud Files * logs will contain log files for both. - * + * * @link http://docs.rackspace.com/loadbalancers/api/v1.0/clb-devguide/content/SSLTermination-d1e2479.html */ -class SSLTermination extends SubResource +class SSLTermination extends NonIdUriResource { - /** * The certificate used for SSL termination. - * - * @var string + * + * @var string */ public $certificate; - + /** - * Determines if the load balancer is enabled to terminate SSL traffic. - * If set to FALSE, the load balancer will retain its specified SSL + * Determines if the load balancer is enabled to terminate SSL traffic. + * If set to FALSE, the load balancer will retain its specified SSL * attributes, but will not terminate SSL traffic. - * - * @var bool + * + * @var bool */ public $enabled; - + /** - * Determines if the load balancer may accept only secure traffic. + * Determines if the load balancer may accept only secure traffic. * If set to TRUE, the load balancer will not accept non-secure traffic. - * - * @var bool + * + * @var bool */ public $secureTrafficOnly; - + /** * The private key for the SSL certificate. - * - * @var string + * + * @var string */ public $privatekey; - + /** * The user's intermediate certificate used for SSL termination. - * - * @var string + * + * @var string */ public $intermediateCertificate; - + /** * The port on which the SSL termination load balancer will listen for secure traffic. - * - * @var int + * + * @var int */ public $securePort; - + protected static $json_name = "sslTermination"; protected static $url_resource = "ssltermination"; + protected $createKeys = array( 'certificate', 'enabled', @@ -86,9 +92,8 @@ class SSLTermination extends SubResource 'securePort' ); - public function create($params = array()) - { - return $this->update($params); + public function create($params = array()) + { + return $this->update($params); } - } diff --git a/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/SessionPersistence.php b/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/SessionPersistence.php index 447c5d4fb..f9c7cb4f0 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/SessionPersistence.php +++ b/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/SessionPersistence.php @@ -1,44 +1,50 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\LoadBalancer\Resource; /** - * Session persistence is a feature of the load balancing service that forces - * multiple requests, of the same protocol, from clients to be directed to the - * same node. This is common with many web applications that do not inherently - * share application state between back-end servers. Two session persistence + * Session persistence is a feature of the load balancing service that forces + * multiple requests, of the same protocol, from clients to be directed to the + * same node. This is common with many web applications that do not inherently + * share application state between back-end servers. Two session persistence * modes are available, as described in the following table: - * - * * HTTP_COOKIE: A session persistence mechanism that inserts an HTTP cookie - * and is used to determine the destination back-end node. This is supported + * + * * HTTP_COOKIE: A session persistence mechanism that inserts an HTTP cookie + * and is used to determine the destination back-end node. This is supported * for HTTP load balancing only. - * - * * SOURCE_IP: A session persistence mechanism that will keep track of the - * source IP address that is mapped and is able to determine the destination - * back-end node. This is supported for HTTPS pass-through and non-HTTP + * + * * SOURCE_IP: A session persistence mechanism that will keep track of the + * source IP address that is mapped and is able to determine the destination + * back-end node. This is supported for HTTPS pass-through and non-HTTP * load balancing only. */ -class SessionPersistence extends SubResource +class SessionPersistence extends NonIdUriResource { /** - * Mode in which session persistence mechanism operates. Can either be set + * Mode in which session persistence mechanism operates. Can either be set * to HTTP_COOKIE or SOURCE_IP. - * - * @var string + * + * @var string */ public $persistenceType; - + protected static $json_name = 'sessionPersistence'; protected static $url_resource = 'sessionpersistence'; - protected $createKeys = array('persistenceType'); + protected $createKeys = array('persistenceType'); } diff --git a/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/Stats.php b/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/Stats.php index 34b91e6b1..52083b666 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/Stats.php +++ b/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/Stats.php @@ -1,71 +1,80 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\LoadBalancer\Resource; /** * Returns statistics about the load balancer. - * + * * @link http://docs.rackspace.com/loadbalancers/api/v1.0/clb-devguide/content/List_Load_Balancer_Stats-d1e1524.html */ -class Stats extends Readonly +class Stats extends ReadOnlyResource { - /** - * Connections closed by this load balancer because the 'connect_timeout' + * Connections closed by this load balancer because the 'connect_timeout' * interval was exceeded. - * - * @var int + * + * @var int */ public $connectTimeOut; - + /** * Number of transaction or protocol errors in this load balancer. - * - * @var int + * + * @var int */ public $connectError; - + /** * Number of connection failures in this load balancer. - * - * @var int + * + * @var int */ public $connectFailure; - + /** - * Connections closed by this load balancer because the 'timeout' interval + * Connections closed by this load balancer because the 'timeout' interval * was exceeded. - * - * @var int + * + * @var int */ public $dataTimedOut; - + /** - * Connections closed by this load balancer because the 'keepalive_timeout' + * Connections closed by this load balancer because the 'keepalive_timeout' * interval was exceeded. - * - * @var int + * + * @var int */ public $keepAliveTimedOut; - + /** - * Maximum number of simultaneous TCP connections this load balancer has + * Maximum number of simultaneous TCP connections this load balancer has * processed at any one time. - * - * @var int + * + * @var int */ public $maxConn; protected static $json_name = false; protected static $url_resource = 'stats'; + public function refresh($id = null, $url = null) + { + return $this->refreshFromParent(); + } } diff --git a/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/SubResource.php b/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/SubResource.php deleted file mode 100644 index 216820297..000000000 --- a/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/SubResource.php +++ /dev/null @@ -1,109 +0,0 @@ - - * @author Jamie Hannaford - */ - -namespace OpenCloud\LoadBalancer\Resource; - -use Guzzle\Http\Exception\ClientErrorResponseException; -use OpenCloud\Common\PersistentObject; -use OpenCloud\Common\Lang; - -/** - * SubResource is an abstract class that handles subresources of a - * LoadBalancer object; for example, the - * `/loadbalancers/{id}/errorpage`. Since most of the subresources are - * handled in a similar manner, this consolidates the functions. - * - * There are really four pieces of data that define a subresource: - * * `$url_resource` - the actual name of the url component - * * `$json_name` - the name of the JSON object holding the data - * * `$json_collection_name` - if the collection is not simply - * `$json_name . 's'`, this defines the collectio name. - * * `$json_collection_element` - if the object in a collection is not - * anonymous, this defines the name of the element holding the object. - * Of these, only the `$json_name` and `$url_resource` are required. - */ -abstract class SubResource extends PersistentObject -{ - /** - * This method needs attention. - * - * @codeCoverageIgnore - */ - public function initialRefresh() - { - if (isset($this->id)) { - $this->refresh(); - } else { - $entity = (method_exists($this->getParent(), 'url')) ? $this->getParent() : $this->getService(); - $this->refresh(null, $entity->url($this->resourceName())); - } - } - - /** - * returns the JSON document's object for creating the subresource - * - * The value `$_create_keys` should be an array of names of data items - * that can be used in the creation of the object. - * - * @return \stdClass; - */ - protected function createJson() - { - $object = new \stdClass; - - foreach ($this->createKeys as $item) { - $object->$item = $this->$item; - } - - if ($top = $this->jsonName()) { - $object = (object) array($top => $object); - } - - return $object; - } - - /** - * returns the JSON for the update (same as create) - * - * For these subresources, the update JSON is the same as the Create JSON - * @return \stdClass - */ - protected function updateJson($params = array()) - { - return $this->createJson(); - } - - /** - * returns a (default) name of the object - * - * The name is constructed by the object class and the object's ID. - * - * @api - * @return string - */ - public function name() - { - $classArray = explode('\\', get_class($this)); - return method_exists($this->getParent(), 'id') - ? sprintf('%s-%s', end($classArray), $this->getParent()->id()) - : parent::name(); - } - - public function refresh($id = null, $url = null) - { - try { - return parent::refresh($id, $url); - } catch (ClientErrorResponseException $e) { - return false; - } - } - -} \ No newline at end of file diff --git a/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/Usage.php b/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/Usage.php deleted file mode 100644 index bbf99d8d1..000000000 --- a/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/Usage.php +++ /dev/null @@ -1,50 +0,0 @@ - - * @author Jamie Hannaford - */ - -namespace OpenCloud\LoadBalancer\Resource; - -/** - * Reports all usage for a Load Balancer recorded within the preceding 24 hours. - */ -class Usage extends Readonly -{ - - public $id; - public $averageNumConnections; - - /** - * Incoming transfer in bytes. - * - * @var int - */ - public $incomingTransfer; - - /** - * Outgoing transfer in bytes. - * - * @var int - */ - public $outgoingTransfer; - public $averageNumConnectionsSsl; - public $incomingTransferSsl; - public $outgoingTransferSsl; - public $numVips; - public $numPolls; - public $startTime; - public $endTime; - public $vipType; - public $sslMode; - public $eventType; - - protected static $json_name = 'loadBalancerUsageRecord'; - protected static $url_resource = 'usage'; - -} diff --git a/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/UsageRecord.php b/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/UsageRecord.php new file mode 100644 index 000000000..108720dbb --- /dev/null +++ b/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/UsageRecord.php @@ -0,0 +1,59 @@ +refreshFromParent(); + } +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/VirtualIp.php b/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/VirtualIp.php index d31807772..7f3935031 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/VirtualIp.php +++ b/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Resource/VirtualIp.php @@ -1,60 +1,64 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\LoadBalancer\Resource; /** - * A virtual IP (VIP) makes a load balancer accessible by clients. The load - * balancing service supports either a public VIP, routable on the public - * Internet, or a ServiceNet address, routable only within the region in which + * A virtual IP (VIP) makes a load balancer accessible by clients. The load + * balancing service supports either a public VIP, routable on the public + * Internet, or a ServiceNet address, routable only within the region in which * the load balancer resides. */ -class VirtualIp extends SubResource +class VirtualIp extends NonIdUriResource { - public $id; - + /** * IP address. - * - * @var string + * + * @var string */ public $address; - + /** * Either "PUBLIC" (public Internet) or "SERVICENET" (internal Rackspace network) - * - * @var int + * + * @var int */ public $type; - + /** * Either 4 or 6. - * - * @var int + * + * @var int */ public $ipVersion; protected static $json_collection_name = 'virtualIps'; - protected static $json_name = FALSE; + protected static $json_name = false; protected static $url_resource = 'virtualips'; - + public $createKeys = array( - 'type', + 'type', 'ipVersion' ); - - public function update($params = array()) - { - return $this->noUpdate(); - } + public function update($params = array()) + { + return $this->noUpdate(); + } } diff --git a/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Service.php b/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Service.php index 4893c571c..7f8d01f74 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Service.php +++ b/rackspace/php-opencloud/lib/OpenCloud/LoadBalancer/Service.php @@ -1,21 +1,30 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\LoadBalancer; +use OpenCloud\Common\Log\Logger; use OpenCloud\Common\Service\NovaService; -use OpenCloud\OpenStack; +use OpenCloud\LoadBalancer\Collection\LoadBalancerIterator; /** - * The Rackspace Cloud Load Balancers + * Class that encapsulates the Rackspace Cloud Load Balancers service + * + * @package OpenCloud\LoadBalancer */ class Service extends NovaService { @@ -23,82 +32,89 @@ class Service extends NovaService const DEFAULT_TYPE = 'rax:load-balancer'; /** - * creates a new LoadBalancer object + * Return a Load Balancer * - * @api - * @param string $id the identifier of the load balancer - * @return Resource\LoadBalancer + * @param string $id + * @return \OpenCloud\LoadBalancer\Resource\LoadBalancer */ - public function loadBalancer($id = null) + public function loadBalancer($id = null) { - return new Resource\LoadBalancer($this, $id); + return $this->resource('LoadBalancer', $id); } /** - * returns a Collection of LoadBalancer objects + * Return a paginated collection of load balancers * - * @api - * @param boolean $detail if TRUE (the default), then all details are - * returned; otherwise, the minimal set (ID, name) are retrieved - * @param array $filter if provided, a set of key/value pairs that are - * set as query string parameters to the query - * @return \OpenCloud\Common\Collection + * @param bool $detail If TRUE, all details are returned; otherwise, a + * minimal set (ID, name) is retrieved [DEPRECATED] + * @param array $filter Optional query params used for search + * @return \OpenCloud\Common\Collection\PaginatedIterator */ - public function loadBalancerList($detail = true, $filter = array()) + public function loadBalancerList($detail = true, array $filter = array()) { - return $this->collection('OpenCloud\LoadBalancer\Resource\LoadBalancer'); + $options = $this->makeResourceIteratorOptions($this->resolveResourceClass('LoadBalancer')); + + if (isset($filter['limit'])) { + $options['limit.page'] = $filter['limit']; + unset($filter['limit']); + } + + $url = $this->getUrl(); + $url->addPath(Resource\LoadBalancer::resourceName()); + $url->setQuery($filter); + + $options = array_merge($options, array('baseUrl' => $url, 'key.marker' => 'id')); + + return LoadBalancerIterator::factory($this, $options); } /** - * creates a new BillableLoadBalancer object (read-only) - * - * @api - * @param string $id the identifier of the load balancer - * @return Resource\LoadBalancer + * @deprecated */ - public function billableLoadBalancer($id = null) + public function billableLoadBalancer($id = null) { - return new Resource\BillableLoadBalancer($this, $id); + $this->getLogger()->warning(Logger::deprecated(__METHOD__, 'loadBalancer')); + + return $this->resource('LoadBalancer', $id); } /** - * returns a Collection of BillableLoadBalancer objects + * Returns a paginated collection of load balancers that have been billed + * between a certain period. * - * @api - * @param boolean $detail if TRUE (the default), then all details are - * returned; otherwise, the minimal set (ID, name) are retrieved - * @param array $filter if provided, a set of key/value pairs that are - * set as query string parameters to the query - * @return \OpenCloud\Common\Collection + * @link http://docs.rackspace.com/loadbalancers/api/v1.0/clb-devguide/content/List_Usage-d1e3014.html + * @param array $filter + * @return \OpenCloud\Common\Collection\PaginatedIterator */ - public function billableLoadBalancerList($detail = true, $filter = array()) + public function billableLoadBalancerList(array $filter = array()) { - $class = 'OpenCloud\LoadBalancer\Resource\BillableLoadBalancer'; - $url = $this->url($class::ResourceName(), $filter); - return $this->collection($class, $url); + $url = $this->getUrl(); + $url->addPath(Resource\LoadBalancer::resourceName()); + $url->addPath('billable'); + $url->setQuery($filter); + + return $this->resourceList('LoadBalancer', $url); } /** - * returns allowed domain + * Returns an allowed domain * - * @api * @param mixed $data either an array of values or null - * @return Resource\AllowedDomain + * @return \OpenCloud\LoadBalancer\Resource\AllowedDomain */ - public function allowedDomain($data = null) + public function allowedDomain($data = null) { - return new Resource\AllowedDomain($this, $data); + return $this->resource('AllowedDomain', $data); } /** - * returns Collection of AllowedDomain object + * Returns Collection of AllowedDomain object * - * @api - * @return Collection + * @return \OpenCloud\Common\Collection\PaginatedIterator */ - public function allowedDomainList() + public function allowedDomainList() { - return $this->collection('OpenCloud\LoadBalancer\Resource\AllowedDomain', null, $this); + return $this->resourceList('AllowedDomain'); } /** @@ -106,22 +122,21 @@ public function allowedDomainList() * * Convenience method to be used by the ProtocolList Collection. * - * @return Resource\Protocol + * @return \OpenCloud\LoadBalancer\Resource\Protocol */ - public function protocol($data = null) + public function protocol($data = null) { - return new Resource\Protocol($this, $data); + return $this->resource('Protocol', $data); } /** - * a list of Protocol objects + * Returns a list of Protocol objects * - * @api - * @return \OpenCloud\Common\Collection + * @return \OpenCloud\Common\Collection\PaginatedIterator */ - public function protocolList() + public function protocolList() { - return $this->collection('OpenCloud\LoadBalancer\Resource\Protocol', null, $this); + return $this->resourceList('Protocol'); } /** @@ -129,22 +144,20 @@ public function protocolList() * * convenience method used by the Collection factory * - * @return Resource\Algorithm + * @return \OpenCloud\LoadBalancer\Resource\Algorithm */ - public function algorithm($data = null) + public function algorithm($data = null) { - return new Resource\Algorithm($this, $data); + return $this->resource('Algorithm', $data); } /** - * a list of Algorithm objects + * Return a list of Algorithm objects * - * @api - * @return \OpenCloud\Common\Collection + * @return \OpenCloud\Common\Collection\PaginatedIterator */ - public function algorithmList() + public function algorithmList() { - return $this->collection('OpenCloud\LoadBalancer\Resource\Algorithm', null, $this); + return $this->resourceList('Algorithm'); } - } diff --git a/rackspace/php-opencloud/lib/OpenCloud/Networking/Resource/Network.php b/rackspace/php-opencloud/lib/OpenCloud/Networking/Resource/Network.php new file mode 100644 index 000000000..19454445e --- /dev/null +++ b/rackspace/php-opencloud/lib/OpenCloud/Networking/Resource/Network.php @@ -0,0 +1,77 @@ + 'adminStateUp', + 'tenant_id' => 'tenantId' + ); + + protected $createKeys = array( + 'adminStateUp', + 'name', + 'shared', + 'tenantId' + ); + + protected $updateKeys = array( + 'name' + ); + + /** + * This method is inherited. The inherited method has protected scope + * but we are widening the scope to public so this method may be called + * from other classes such as {@see OpenCloud\Networking\Service}. + */ + public function createJson() + { + return parent::createJson(); + } + + public function getId() + { + return $this->id; + } +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Networking/Resource/NetworkInterface.php b/rackspace/php-opencloud/lib/OpenCloud/Networking/Resource/NetworkInterface.php new file mode 100644 index 000000000..30fe10de2 --- /dev/null +++ b/rackspace/php-opencloud/lib/OpenCloud/Networking/Resource/NetworkInterface.php @@ -0,0 +1,31 @@ + 'adminStateUp', + 'network_id' => 'networkId', + 'device_id' => 'deviceId', + 'device_owner' => 'deviceOwner', + 'fixed_ips' => 'fixedIps', + 'mac_address' => 'macAddress', + 'security_groups' => 'securityGroups', + 'tenant_id' => 'tenantId', + 'subnet_id' => 'subnetId', + 'ip_address' => 'ipAddress' + ); + + protected $createKeys = array( + 'name', + 'adminStateUp', + 'networkId', + 'deviceId', + 'deviceOwner', + 'fixedIps', + 'macAddress', + 'securityGroups', + 'tenantId' + ); + + protected $updateKeys = array( + 'name', + 'deviceId', + 'securityGroups' + ); + + /** + * This method is inherited. The inherited method has protected scope + * but we are widening the scope to public so this method may be called + * from other classes such as {@see OpenCloud\Networking\Service}. + */ + public function createJson() + { + return parent::createJson(); + } +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Networking/Resource/SecurityGroup.php b/rackspace/php-opencloud/lib/OpenCloud/Networking/Resource/SecurityGroup.php new file mode 100644 index 000000000..4b2f85efd --- /dev/null +++ b/rackspace/php-opencloud/lib/OpenCloud/Networking/Resource/SecurityGroup.php @@ -0,0 +1,69 @@ + 'securityGroupRules', + 'tenant_id' => 'tenantId' + ); + + protected $createKeys = array( + 'name', + 'description' + ); + + /** + * This method is inherited. The inherited method has protected scope + * but we are widening the scope to public so this method may be called + * from other classes such as {@see OpenCloud\Networking\Service}. + */ + public function createJson() + { + return parent::createJson(); + } + + /** + * {@inheritDoc} + */ + public function update($params = array()) + { + return $this->noUpdate(); + } +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Networking/Resource/SecurityGroupRule.php b/rackspace/php-opencloud/lib/OpenCloud/Networking/Resource/SecurityGroupRule.php new file mode 100644 index 000000000..06ad028b2 --- /dev/null +++ b/rackspace/php-opencloud/lib/OpenCloud/Networking/Resource/SecurityGroupRule.php @@ -0,0 +1,86 @@ + 'portRangeMin', + 'port_range_max' => 'portRangeMax', + 'remote_group_id' => 'remoteGroupId', + 'remote_ip_prefix' => 'remoteIpPrefix', + 'security_group_id' => 'securityGroupId', + 'tenant_id' => 'tenantId' + ); + + protected $createKeys = array( + 'direction', + 'ethertype', + 'securityGroupId', + 'portRangeMin', + 'portRangeMax', + 'protocol', + 'remoteGroupId', + 'remoteIpPrefix' + ); + + /** + * This method is inherited. The inherited method has protected scope + * but we are widening the scope to public so this method may be called + * from other classes such as {@see OpenCloud\Networking\Service}. + */ + public function createJson() + { + return parent::createJson(); + } + + /** + * {@inheritDoc} + */ + public function update($params = array()) + { + return $this->noUpdate(); + } +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Networking/Resource/Subnet.php b/rackspace/php-opencloud/lib/OpenCloud/Networking/Resource/Subnet.php new file mode 100644 index 000000000..5b15220f9 --- /dev/null +++ b/rackspace/php-opencloud/lib/OpenCloud/Networking/Resource/Subnet.php @@ -0,0 +1,89 @@ + 'enableDhcp', + 'network_id' => 'networkId', + 'dns_nameservers' => 'dnsNameservers', + 'allocation_pools' => 'allocationPools', + 'host_routes' => 'hostRoutes', + 'ip_version' => 'ipVersion', + 'gateway_ip' => 'gatewayIp', + 'tenant_id' => 'tenantId' + ); + + protected $createKeys = array( + 'name', + 'enableDhcp', + 'networkId', + 'allocationPools', + 'hostRoutes', + 'ipVersion', + 'gatewayIp', + 'cidr', + 'tenantId' + ); + + protected $updateKeys = array( + 'name', + 'enableDhcp', + 'hostRoutes', + 'gatewayIp' + ); + + /** + * This method is inherited. The inherited method has protected scope + * but we are widening the scope to public so this method may be called + * from other classes such as {@see OpenCloud\Networking\Service}. + */ + public function createJson() + { + return parent::createJson(); + } +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Networking/Service.php b/rackspace/php-opencloud/lib/OpenCloud/Networking/Service.php new file mode 100644 index 000000000..e3fcc879e --- /dev/null +++ b/rackspace/php-opencloud/lib/OpenCloud/Networking/Service.php @@ -0,0 +1,412 @@ +resource('Network', $id); + } + + /** + * Creates a new Network and returns it. + * + * @param array $params Network creation parameters. + * @return \OpenCloud\Networking\Resource\Network Object representing created network + * + * @see https://github.com/rackspace/php-opencloud/blob/master/docs/userguide/Networking/USERGUIDE.md#create-a-network + */ + public function createNetwork(array $params = array()) + { + $network = $this->network(); + $network->create($params); + return $network; + } + + /** + * Creates multiple new Networks and returns their list. + * + * @param array $networksParams Array of network creation parameters' arrays + * @return \OpenCloud\Common\Collection\PaginatedIterator + */ + public function createNetworks(array $networksParams = array()) + { + // Form URL + $url = clone $this->getUrl(); + $url->addPath(Network::resourceName()); + + // Form JSON + $singleNetworkJsonName = Network::jsonName(); + $networksJsonCollectionName = Network::jsonCollectionName(); + $networks = array(); + foreach ($networksParams as $networkParams) { + $network = $this->network(); + $network->populate($networkParams); + $networks[] = $network->createJson()->{$singleNetworkJsonName}; + } + $json = json_encode(array( + $networksJsonCollectionName => $networks + )); + + // Call the API + $response = $this->getClient()->post($url, self::getJsonHeader(), $json)->send(); + + // Parse the response into a collection of created networks + $responseJson = Formatter::decode($response); + $createdNetworksJson = $responseJson->{$networksJsonCollectionName}; + + // Return collection of created networks + return $this->collection('Network', $url, $this, $createdNetworksJson); + } + + /** + * Returns a Network object associated with this Networking service + * + * @param string $id ID of network to retrieve + * @return \OpenCloud\Networking\Resource\Network object + */ + public function getNetwork($id) + { + return $this->network($id); + } + + /** + * Returns a list of networks you created + * + * @param array $params + * @return \OpenCloud\Common\Collection\PaginatedIterator + */ + public function listNetworks(array $params = array()) + { + $url = clone $this->getUrl(); + $url->addPath(Network::resourceName())->setQuery($params); + + return $this->resourceList('Network', $url); + } + + /** + * Returns a Subnet object associated with this Networking service + * + * @param string $id ID of subnet to retrieve + * @return \OpenCloud\Networking\Resource\Subnet object + */ + public function subnet($id = null) + { + return $this->resource('Subnet', $id); + } + + /** + * Creates a new Subnet and returns it. + * + * @param array $params Subnet creation parameters. + * @return \OpenCloud\Networking\Resource\Subnet Object representing created subnet + * + * @see https://github.com/rackspace/php-opencloud/blob/master/docs/userguide/Networking/USERGUIDE.md#create-a-subnet + */ + public function createSubnet(array $params = array()) + { + $subnet = $this->subnet(); + $subnet->create($params); + return $subnet; + } + + /** + * Creates multiple new Subnets and returns their list. + * + * @param array $subnetsParams Array of subnet creation parameters' arrays + * @return \OpenCloud\Common\Collection\PaginatedIterator + */ + public function createSubnets(array $subnetsParams = array()) + { + // Form URL + $url = clone $this->getUrl(); + $url->addPath(Subnet::resourceName()); + + // Form JSON + $singleSubnetJsonName = Subnet::jsonName(); + $subnetsJsonCollectionName = Subnet::jsonCollectionName(); + $subnets = array(); + foreach ($subnetsParams as $subnetParams) { + $subnet = $this->subnet(); + $subnet->populate($subnetParams); + $subnets[] = $subnet->createJson()->{$singleSubnetJsonName}; + } + $json = json_encode(array( + $subnetsJsonCollectionName => $subnets + )); + + // Call the API + $response = $this->getClient()->post($url, self::getJsonHeader(), $json)->send(); + + // Parse the response into a collection of created subnets + $responseJson = Formatter::decode($response); + $createdSubnetsJson = $responseJson->{$subnetsJsonCollectionName}; + + // Return collection of created subnets + return $this->collection('Subnet', $url, $this, $createdSubnetsJson); + } + + /** + * Returns a Subnet object associated with this Networking service + * + * @param string $id ID of subnet to retrieve + * @return \OpenCloud\Networking\Resource\Subnet object + */ + public function getSubnet($id) + { + return $this->subnet($id); + } + + /** + * Returns a list of subnets you created + * + * @param array $params + * @return \OpenCloud\Common\Collection\PaginatedIterator + */ + public function listSubnets(array $params = array()) + { + $url = clone $this->getUrl(); + $url->addPath(Subnet::resourceName())->setQuery($params); + + return $this->resourceList('Subnet', $url); + } + + /** + * Returns a Port object associated with this Networking service + * + * @param string $id ID of port to retrieve + * @return \OpenCloud\Networking\Resource\Port object + */ + public function port($id = null) + { + return $this->resource('Port', $id); + } + + /** + * Creates a new Port and returns it. + * + * @param array $params Port creation parameters. + * @return \OpenCloud\Networking\Resource\Port Object representing created port + * + * @see https://github.com/rackspace/php-opencloud/blob/master/docs/userguide/Networking/USERGUIDE.md#create-a-port + */ + public function createPort(array $params = array()) + { + $port = $this->port(); + $port->create($params); + return $port; + } + + /** + * Creates multiple new Ports and returns their list. + * + * @param array $portsParams Array of port creation parameters' arrays + * @return \OpenCloud\Common\Collection\PaginatedIterator + */ + public function createPorts(array $portsParams = array()) + { + // Form URL + $url = clone $this->getUrl(); + $url->addPath(Port::resourceName()); + + // Form JSON + $singlePortJsonName = Port::jsonName(); + $portsJsonCollectionName = Port::jsonCollectionName(); + $ports = array(); + foreach ($portsParams as $portParams) { + $port = $this->port(); + $port->populate($portParams); + $ports[] = $port->createJson()->{$singlePortJsonName}; + } + $json = json_encode(array( + $portsJsonCollectionName => $ports + )); + + // Call the API + $response = $this->getClient()->post($url, self::getJsonHeader(), $json)->send(); + + // Parse the response into a collection of created ports + $responseJson = Formatter::decode($response); + $createdPortsJson = $responseJson->{$portsJsonCollectionName}; + + // Return collection of created ports + return $this->collection('Port', $url, $this, $createdPortsJson); + } + + /** + * Returns a Port object associated with this Networking service + * + * @param string $id ID of port to retrieve + * @return \OpenCloud\Networking\Resource\Port object + */ + public function getPort($id) + { + return $this->port($id); + } + + /** + * Returns a list of ports you created + * + * @param array $params + * @return \OpenCloud\Common\Collection\PaginatedIterator + */ + public function listPorts(array $params = array()) + { + $url = clone $this->getUrl(); + $url->addPath(Port::resourceName())->setQuery($params); + + return $this->resourceList('Port', $url); + } + + /** + * Returns a SecurityGroup object associated with this Networking service + * + * @param string $id ID of security group to retrieve + * @return \OpenCloud\Networking\Resource\SecurityGroup object + */ + public function securityGroup($id = null) + { + return $this->resource('SecurityGroup', $id); + } + + /** + * Creates a new SecurityGroup and returns it. + * + * @param array $params SecurityGroup creation parameters. + * @return \OpenCloud\Networking\Resource\SecurityGroup Object representing created security group + * + * @see https://github.com/rackspace/php-opencloud/blob/master/docs/userguide/Networking/USERGUIDE.md#create-a-security-group + */ + public function createSecurityGroup(array $params = array()) + { + $securityGroup = $this->securityGroup(); + $securityGroup->create($params); + return $securityGroup; + } + + /** + * Returns a SecurityGroup object associated with this Networking service + * + * @param string $id ID of security group to retrieve + * @return \OpenCloud\Networking\Resource\SecurityGroup object + */ + public function getSecurityGroup($id) + { + return $this->securityGroup($id); + } + + /** + * Returns a list of security groups you created + * + * @param array $params + * @return \OpenCloud\Common\Collection\PaginatedIterator + */ + public function listSecurityGroups(array $params = array()) + { + $url = clone $this->getUrl(); + $url->addPath(SecurityGroup::resourceName())->setQuery($params); + + return $this->resourceList('SecurityGroup', $url); + } + + /** + * Returns a SecurityGroupRule object associated with this Networking service + * + * @param string $id ID of security group rule to retrieve + * @return \OpenCloud\Networking\Resource\SecurityGroupRule object + */ + public function securityGroupRule($id = null) + { + return $this->resource('SecurityGroupRule', $id); + } + + /** + * Creates a new SecurityGroupRule and returns it. + * + * @param array $params SecurityGroupRule creation parameters. + * @return \OpenCloud\Networking\Resource\SecurityGroupRule Object representing created security group rule + * + * @see https://github.com/rackspace/php-opencloud/blob/master/docs/userguide/Networking/USERGUIDE.md#create-a-security-group-rule + */ + public function createSecurityGroupRule(array $params = array()) + { + $securityGroupRule = $this->securityGroupRule(); + $securityGroupRule->create($params); + return $securityGroupRule; + } + + /** + * Returns a SecurityGroupRule object associated with this Networking service + * + * @param string $id ID of security group rule to retrieve + * @return \OpenCloud\Networking\Resource\SecurityGroupRule object + */ + public function getSecurityGroupRule($id) + { + return $this->securityGroupRule($id); + } + + /** + * Returns a list of security group rules you created + * + * @param array $params + * @return \OpenCloud\Common\Collection\PaginatedIterator + */ + public function listSecurityGroupRules(array $params = array()) + { + $url = clone $this->getUrl(); + $url->addPath(SecurityGroupRule::resourceName())->setQuery($params); + + return $this->resourceList('SecurityGroupRule', $url); + } + + /** + * Return namespaces. + * + * @return array + */ + public function namespaces() + { + return array(); + } +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/AbstractService.php b/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/AbstractService.php index 241426e75..6094d7780 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/AbstractService.php +++ b/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/AbstractService.php @@ -1,11 +1,18 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\ObjectStore; @@ -19,27 +26,13 @@ abstract class AbstractService extends CatalogService { const MAX_CONTAINER_NAME_LENGTH = 256; - const MAX_OBJECT_NAME_LEN = 1024; - const MAX_OBJECT_SIZE = 5102410241025; + const MAX_OBJECT_NAME_LEN = 1024; + const MAX_OBJECT_SIZE = 5102410241025; /** - * List all available containers. If called by a CDN service, it returns CDN-enabled; if called by a regular - * service, normal containers are returned. + * Returns the Object Store account associated with the service. * - * @param array $filter - * @return Collection - */ - public function listContainers(array $filter = array()) - { - $filter['format'] = 'json'; - - $class = ($this instanceof Service) ? 'Container' : 'CDNContainer'; - - return $this->resourceList($class, $this->getUrl(null, $filter), $this); - } - - /** - * @return Resource\Account + * @return Resource\Account Object Store account */ public function getAccount() { diff --git a/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/CDNService.php b/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/CDNService.php index 4e070991a..6a13a8ee0 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/CDNService.php +++ b/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/CDNService.php @@ -1,22 +1,73 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\ObjectStore; -use OpenCloud\OpenStack; +use OpenCloud\ObjectStore\Resource\CDNContainer; +use OpenCloud\ObjectStore\Resource\ContainerMetadata; /** - * This is the CDN version of the ObjectStore service. + * This is the CDN version of the ObjectStore service. */ class CDNService extends AbstractService { const DEFAULT_NAME = 'cloudFilesCDN'; const DEFAULT_TYPE = 'rax:object-cdn'; + + /** + * List CDN-enabled containers. + * + * @param array $filter Array of filter options such as: + * + * * `limit`: number of results to limit the list to. Optional. + * * `marker`: name of container after which to start the list. Optional. + * * `end_marker`: name of container before which to end the list. Optional. + * @return \OpenCloud\Common\Collection\PaginatedIterator Iterator to list of CDN-enabled containers + */ + public function listContainers(array $filter = array()) + { + $filter['format'] = 'json'; + return $this->resourceList('CDNContainer', $this->getUrl(null, $filter), $this); + } + + /** + * Return an existing CDN-enabled container. + * + * @param \stdClass $data Data to initialize container. + * @return CDNContainer CDN-enabled Container + */ + public function cdnContainer($data) + { + $container = new CDNContainer($this, $data); + + if (is_object($data)) { + $metadata = new ContainerMetadata(); + $metadata->setArray(array( + 'Streaming-Uri' => $data->cdn_streaming_uri, + 'Ios-Uri' => $data->cdn_ios_uri, + 'Ssl-Uri' => $data->cdn_ssl_uri, + 'Enabled' => $data->cdn_enabled, + 'Ttl' => $data->ttl, + 'Log-Retention' => $data->log_retention, + 'Uri' => $data->cdn_uri, + )); + $container->setMetadata($metadata); + } + + return $container; + } } diff --git a/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Constants/Header.php b/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Constants/Header.php index 7167807b0..9ab4b5047 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Constants/Header.php +++ b/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Constants/Header.php @@ -1,10 +1,18 @@ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\ObjectStore\Constants; @@ -12,13 +20,13 @@ /** * Constants for different request and metadata headers. */ -class Header +class Header { const OBJECT_COUNT = 'Object-Count'; - const BYTES_USED = 'Bytes-Used'; - const ACCESS_LOGS = 'Access-Log-Delivery'; + const BYTES_USED = 'Bytes-Used'; + const ACCESS_LOGS = 'Access-Log-Delivery'; const TRANS_ID = 'Trans-Id'; - const ENABLED = 'Enabled'; + const ENABLED = 'Enabled'; const LOG_RETENTION = 'Log-Retention'; -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Constants/UrlType.php b/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Constants/UrlType.php index ef708fc65..851c081f2 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Constants/UrlType.php +++ b/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Constants/UrlType.php @@ -1,10 +1,18 @@ + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\ObjectStore\Constants; @@ -18,8 +26,8 @@ class UrlType const SSL = 'SSL'; const STREAMING = 'Streaming'; const IOS_STREAMING = 'IOS-Streaming'; - + const TAR = 'tar'; const TAR_GZ = 'tar.gz'; const TAR_BZ2 = 'tar.bz2'; -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Enum/ReturnType.php b/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Enum/ReturnType.php new file mode 100644 index 000000000..ac9193ed4 --- /dev/null +++ b/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Enum/ReturnType.php @@ -0,0 +1,29 @@ + + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\ObjectStore\Exception; @@ -14,13 +22,13 @@ class BulkOperationException extends \Exception public function __construct(array $errors) { $output = ''; - + foreach ($errors as $error) { $output .= "$error[0]: $error[1]" . PHP_EOL; } - + parent::__construct( 'These errors occurred while performing an archive upload: ' . $output ); } -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Exception/ContainerException.php b/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Exception/ContainerException.php index bf7f53187..bd3b96bad 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Exception/ContainerException.php +++ b/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Exception/ContainerException.php @@ -1,14 +1,22 @@ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\ObjectStore\Exception; class ContainerException extends \Exception { -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Exception/ObjectNotEmptyException.php b/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Exception/ObjectNotEmptyException.php new file mode 100644 index 000000000..6bf75f2f9 --- /dev/null +++ b/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Exception/ObjectNotEmptyException.php @@ -0,0 +1,35 @@ +name = $name; + + return $e; + } +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Exception/ObjectNotFoundException.php b/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Exception/ObjectNotFoundException.php index 233a69589..05afcd113 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Exception/ObjectNotFoundException.php +++ b/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Exception/ObjectNotFoundException.php @@ -1,4 +1,19 @@ name = $name; $e->response = $exception->getResponse(); - $e->request = $exception->getRequest(); + $e->request = $exception->getRequest(); return $e; } -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Exception/StreamException.php b/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Exception/StreamException.php index afce72756..3bf0ae7ce 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Exception/StreamException.php +++ b/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Exception/StreamException.php @@ -1,12 +1,22 @@ + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\ObjectStore\Exception; -class StreamException extends \Exception {} \ No newline at end of file +class StreamException extends \Exception +{ +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Exception/UploadException.php b/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Exception/UploadException.php index e8393d09c..879eb0ff3 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Exception/UploadException.php +++ b/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Exception/UploadException.php @@ -1,10 +1,18 @@ + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\ObjectStore\Exception; @@ -28,4 +36,4 @@ public function getState() { return $this->state; } -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Resource/AbstractContainer.php b/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Resource/AbstractContainer.php index 87064a608..56cd1c2fa 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Resource/AbstractContainer.php +++ b/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Resource/AbstractContainer.php @@ -1,10 +1,18 @@ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\ObjectStore\Resource; @@ -19,36 +27,51 @@ abstract class AbstractContainer extends AbstractResource { protected $metadataClass = 'OpenCloud\\ObjectStore\\Resource\\ContainerMetadata'; - + /** - * The name of the container. - * - * The only restrictions on container names is that they cannot contain a - * forward slash (/) and must be less than 256 bytes in length. Please note - * that the length restriction applies to the name after it has been URL + * The name of the container. + * + * The only restrictions on container names is that they cannot contain a + * forward slash (/) and must be less than 256 bytes in length. Please note + * that the length restriction applies to the name after it has been URL * encoded. For example, a container named Course Docs would be URL encoded * as Course%20Docs - which is 13 bytes in length rather than the expected 11. - * + * * @var string */ public $name; - + public function __construct(ServiceInterface $service, $data = null) { - $this->service = $service; + $this->service = $service; $this->metadata = new $this->metadataClass; // Populate data if set $this->populate($data); } + /** + * Return the transaction ID for an HTTP API operation. Useful for debugging. + * + * @return string Transaction ID + */ public function getTransId() { return $this->metadata->getProperty(HeaderConst::TRANS_ID); } - public abstract function isCdnEnabled(); - + /** + * Returns whether this container is CDN-enabled or not. + * + * @return boolean true if this container is CDN-enabled; false, otherwise. + */ + abstract public function isCdnEnabled(); + + /** + * Returns whether this container has log retention enabled or not. + * + * @return boolean true if this container has log retention enabled; false, otherwise. + */ public function hasLogRetention() { if ($this instanceof CDNContainer) { @@ -57,12 +80,24 @@ public function hasLogRetention() return $this->metadata->propertyExists(HeaderConst::ACCESS_LOGS); } } - + + /** + * For internal use only + * + * @return string Name of the primary key field for this resource + */ public function primaryKeyField() { return 'name'; } + /** + * For internal use only + * + * @param string $path Path to add to URL. Optional. + * @param array $params Query parameters to add to URL. Optional. + * @return Url URL of this container + path + query parameters. + */ public function getUrl($path = null, array $params = array()) { if (strlen($this->getName()) == 0) { @@ -73,7 +108,7 @@ public function getUrl($path = null, array $params = array()) return $url->addPath((string) $this->getName())->addPath((string) $path)->setQuery($params); } - + protected function createRefreshRequest() { return $this->getClient()->head($this->getUrl(), array('Accept' => '*/*')); @@ -83,7 +118,7 @@ protected function createRefreshRequest() * This method will enable your CDN-enabled container to serve out HTML content like a website. * * @param $indexPage The data object name (i.e. a .html file) that will serve as the main index page. - * @return \Guzzle\Http\Message\Response + * @return \Guzzle\Http\Message\Response The HTTP response for this API operation. */ public function setStaticIndexPage($page) { @@ -94,6 +129,7 @@ public function setStaticIndexPage($page) } $headers = array('X-Container-Meta-Web-Index' => $page); + return $this->getClient()->post($this->getUrl(), $headers)->send(); } @@ -101,7 +137,7 @@ public function setStaticIndexPage($page) * Set the default error page for your static site. * * @param $name The data object name (i.e. a .html file) that will serve as the main error page. - * @return \Guzzle\Http\Message\Response + * @return \Guzzle\Http\Message\Response The HTTP response for this operation. */ public function setStaticErrorPage($page) { @@ -112,6 +148,7 @@ public function setStaticErrorPage($page) } $headers = array('X-Container-Meta-Web-Error' => $page); + return $this->getClient()->post($this->getUrl(), $headers)->send(); } -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Resource/AbstractResource.php b/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Resource/AbstractResource.php index e381a5804..37722e93d 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Resource/AbstractResource.php +++ b/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Resource/AbstractResource.php @@ -1,10 +1,18 @@ + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\ObjectStore\Resource; @@ -14,39 +22,54 @@ use OpenCloud\Common\Service\ServiceInterface; /** - * Abstract base class which implements shared functionality of ObjectStore - * resources. Provides support, for example, for metadata-handling and other + * Abstract base class which implements shared functionality of ObjectStore + * resources. Provides support, for example, for metadata-handling and other * features that are common to the ObjectStore components. */ abstract class AbstractResource extends Base { const GLOBAL_METADATA_PREFIX = 'X'; - /** @var \OpenCloud\Common\Metadata*/ + /** @var \OpenCloud\Common\Metadata */ protected $metadata; /** @var string The FQCN of the metadata object used for the container. */ protected $metadataClass = 'OpenCloud\\Common\\Metadata'; - + /** @var \OpenCloud\Common\Service\ServiceInterface The service object. */ protected $service; - - public function __construct(ServiceInterface $service) + + public function __construct(ServiceInterface $service) { - $this->service = $service; + $this->service = $service; $this->metadata = new $this->metadataClass; } - + + /** + * For internal use only. + * + * @return Service The ObjectStore service associated with this ObjectStore resource. + */ public function getService() { return $this->service; } - + + /** + * For internal use only. + * + * @return Service The CDN version of the ObjectStore service associated with this ObjectStore resource. + */ public function getCdnService() { return $this->service->getCDNService(); } - + + /** + * For internal use only. + * + * @return Client The HTTP client associated with the associated ObjectStore service. + */ public function getClient() { return $this->service->getClient(); @@ -55,31 +78,35 @@ public function getClient() /** * Factory method that allows for easy instantiation from a Response object. * - * @param Response $response - * @param ServiceInterface $service - * @return static + * For internal use only. + * + * @param Response $response HTTP response from an API operation. + * @param ServiceInterface $service The ObjectStore service to associate with this ObjectStore resource object. + * @return AbstractResource A concrete sub-class of `AbstractResource`. */ public static function fromResponse(Response $response, ServiceInterface $service) { $object = new static($service); - + if (null !== ($headers = $response->getHeaders())) { $object->setMetadata($headers, true); } - + return $object; } /** * Trim headers of their resource-specific prefixes. * - * @param $headers - * @return array + * For internal use only. + * + * @param array $headers Headers as returned from an HTTP response + * @return array Trimmed headers */ public static function trimHeaders($headers) { $output = array(); - + foreach ($headers as $header => $value) { // Only allow allow X--* headers to pass through after stripping them if (static::headerIsValidMetadata($header) && ($key = self::stripPrefix($header))) { @@ -93,6 +120,7 @@ public static function trimHeaders($headers) protected static function headerIsValidMetadata($header) { $pattern = sprintf('#^%s\-#i', self::GLOBAL_METADATA_PREFIX); + return preg_match($pattern, $header); } @@ -105,31 +133,45 @@ protected static function headerIsValidMetadata($header) protected static function stripPrefix($header) { $pattern = '#^' . self::GLOBAL_METADATA_PREFIX . '\-(' . static::METADATA_LABEL . '-)?(Meta-)?#i'; + return preg_replace($pattern, '', $header); } /** * Prepend/stock the header names with a resource-specific prefix. * - * @param array $headers - * @return array + * @param array $headers Headers to use on ObjectStore resource. + * @return array Headers returned with appropriate prefix as expected by ObjectStore service. */ public static function stockHeaders(array $headers) { $output = array(); + $prefix = null; + $corsHeaders = array( + 'Access-Control-Allow-Origin', + 'Access-Control-Expose-Headers', + 'Access-Control-Max-Age', + 'Access-Control-Allow-Credentials', + 'Access-Control-Allow-Methods', + 'Access-Control-Allow-Headers' + ); foreach ($headers as $header => $value) { - $prefix = self::GLOBAL_METADATA_PREFIX . '-' . static::METADATA_LABEL . '-Meta-'; + if (!in_array($header, $corsHeaders)) { + $prefix = self::GLOBAL_METADATA_PREFIX . '-' . static::METADATA_LABEL . '-Meta-'; + } $output[$prefix . $header] = $value; } + return $output; } /** - * Set the metadata (local-only) for this object. + * Set the metadata (local-only) for this object. You must call saveMetadata + * to actually persist the metadata using the ObjectStore service. * - * @param $data - * @param bool $constructFromResponse - * @return $this + * @param array $data Object/container metadata key/value pair array. + * @param bool $constructFromResponse Whether the metadata key/value pairs were obtiained from an HTTP response of an ObjectStore API operation. + * @return AbstractResource This object, with metadata set. */ public function setMetadata($data, $constructFromResponse = false) { @@ -140,11 +182,14 @@ public function setMetadata($data, $constructFromResponse = false) } $this->metadata = $data; + return $this; } /** - * @return \OpenCloud\Common\Metadata + * Returns metadata for this object. + * + * @return \OpenCloud\Common\Metadata Metadata set on this object. */ public function getMetadata() { @@ -157,38 +202,40 @@ public function getMetadata() * @param array $metadata The array of values you want to set as metadata * @param bool $stockPrefix Whether to prepend each array key with the metadata-specific prefix. For objects, this * would be X-Object-Meta-Foo => Bar - * @return mixed + * @return Response HTTP response from API operation. */ public function saveMetadata(array $metadata, $stockPrefix = true) { $headers = ($stockPrefix === true) ? self::stockHeaders($metadata) : $metadata; + return $this->getClient()->post($this->getUrl(), $headers)->send(); } /** * Retrieve metadata from the API. This method will then set and return this value. * - * @return \OpenCloud\Common\Metadata + * @return \OpenCloud\Common\Metadata Metadata returned from the ObjectStore service for this object/container. */ public function retrieveMetadata() { $response = $this->getClient() ->head($this->getUrl()) ->send(); - + $this->setMetadata($response->getHeaders(), true); + return $this->metadata; } /** * To delete or unset a particular metadata item. * - * @param $key - * @return mixed + * @param $key Metadata key to unset + * @return Response HTTP response returned from API operation to unset metadata item. */ public function unsetMetadataItem($key) { - $header = sprintf('%s-Remove-%s-Meta-%s', self::GLOBAL_METADATA_PREFIX, + $header = sprintf('%s-Remove-%s-Meta-%s', self::GLOBAL_METADATA_PREFIX, static::METADATA_LABEL, $key); $headers = array($header => 'True'); @@ -199,16 +246,17 @@ public function unsetMetadataItem($key) } /** - * Append a particular array of values to the existing metadata. Analogous to a merge. + * Append a particular array of values to the existing metadata. Analogous + * to a merge. You must call to actually persist the metadata using the + * ObjectStore service. * - * @param array $values - * @return array + * @param array $values The array of values you want to append to metadata. + * @return array Metadata, after `$values` are appended. */ public function appendToMetadata(array $values) { - return (!empty($this->metadata) && is_array($this->metadata)) + return (!empty($this->metadata) && is_array($this->metadata)) ? array_merge($this->metadata, $values) : $values; } - -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Resource/Account.php b/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Resource/Account.php index 331f38238..dd9caf769 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Resource/Account.php +++ b/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Resource/Account.php @@ -1,17 +1,25 @@ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\ObjectStore\Resource; /** * Represents an account that interacts with the CloudFiles API. - * + * * @link http://docs.rackspace.com/files/api/v1/cf-devguide/content/Accounts-d1e421.html */ class Account extends AbstractResource @@ -22,7 +30,7 @@ class Account extends AbstractResource * @var string The temporary URL secret for this account */ private $tempUrlSecret; - + public function getUrl($path = null, array $query = array()) { return $this->getService()->getUrl(); @@ -61,8 +69,8 @@ public function getBytesUsed() { return $this->metadata->getProperty('Bytes-Used'); } - - /** + + /** * Sets the secret value for the temporary URL. * * @link http://docs.rackspace.com/files/api/v1/cf-devguide/content/Set_Account_Metadata-d1a4460.html @@ -70,16 +78,16 @@ public function getBytesUsed() * @param null $secret The value to set the secret to. If left blank, a random hash is generated. * @return $this */ - public function setTempUrlSecret($secret = null) + public function setTempUrlSecret($secret = null) { if (!$secret) { $secret = sha1(rand(1, 99999)); } - + $this->tempUrlSecret = $secret; - + $this->saveMetadata($this->appendToMetadata(array('Temp-Url-Key' => $secret))); - + return $this; } @@ -92,8 +100,7 @@ public function getTempUrlSecret() $this->retrieveMetadata(); $this->tempUrlSecret = $this->metadata->getProperty('Temp-Url-Key'); } - + return $this->tempUrlSecret; } - -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Resource/CDNContainer.php b/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Resource/CDNContainer.php index 31f62cf87..80e547349 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Resource/CDNContainer.php +++ b/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Resource/CDNContainer.php @@ -1,11 +1,18 @@ - * @author Glen Campbell + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\ObjectStore\Resource; @@ -13,8 +20,8 @@ use OpenCloud\ObjectStore\Constants\Header as HeaderConst; /** - * A container that has been CDN-enabled. Each CDN-enabled container has a unique - * Uniform Resource Locator (URL) that can be combined with its object names and + * A container that has been CDN-enabled. Each CDN-enabled container has a unique + * Uniform Resource Locator (URL) that can be combined with its object names and * openly distributed in web pages, emails, or other applications. */ class CDNContainer extends AbstractContainer @@ -68,7 +75,7 @@ public function refresh($name = null, $url = null) $headers = $response->getHeaders(); $this->setMetadata($headers, true); - return $headers; + return $headers; } /** @@ -79,6 +86,7 @@ public function refresh($name = null, $url = null) public function enableCdnLogging() { $headers = array('X-Log-Retention' => 'True'); + return $this->getClient()->put($this->getUrl(), $headers)->send(); } @@ -90,6 +98,7 @@ public function enableCdnLogging() public function disableCdnLogging() { $headers = array('X-Log-Retention' => 'False'); + return $this->getClient()->put($this->getUrl(), $headers)->send(); } @@ -97,4 +106,17 @@ public function isCdnEnabled() { return $this->metadata->getProperty(HeaderConst::ENABLED) == 'True'; } -} \ No newline at end of file + + /** + * Set the TTL. + * + * @param $ttl The time-to-live in seconds. + * @return \Guzzle\Http\Message\Response + */ + public function setTtl($ttl) + { + $headers = array('X-Ttl' => $ttl); + + return $this->getClient()->post($this->getUrl(), $headers)->send(); + } +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Resource/Container.php b/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Resource/Container.php index c0a01cb37..474d9134b 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Resource/Container.php +++ b/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Resource/Container.php @@ -1,11 +1,18 @@ - * @author Glen Campbell + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\ObjectStore\Resource; @@ -23,20 +30,21 @@ use OpenCloud\ObjectStore\Exception\ObjectNotFoundException; use OpenCloud\ObjectStore\Upload\DirectorySync; use OpenCloud\ObjectStore\Upload\TransferBuilder; +use OpenCloud\ObjectStore\Enum\ReturnType; /** - * A container is a storage compartment for your data and provides a way for you - * to organize your data. You can think of a container as a folder in Windows - * or a directory in Unix. The primary difference between a container and these + * A container is a storage compartment for your data and provides a way for you + * to organize your data. You can think of a container as a folder in Windows + * or a directory in Unix. The primary difference between a container and these * other file system concepts is that containers cannot be nested. - * + * * A container can also be CDN-enabled (for public access), in which case you * will need to interact with a CDNContainer object instead of this one. */ class Container extends AbstractContainer { const METADATA_LABEL = 'Container'; - + /** * This is the object that holds all the CDN functionality. This Container therefore acts as a simple wrapper and is * interested in storage concerns only. @@ -61,17 +69,17 @@ public function __construct(ServiceInterface $service, $data = null) /** * Factory method that instantiates an object from a Response object. * - * @param Response $response + * @param Response $response * @param ServiceInterface $service * @return static */ public static function fromResponse(Response $response, ServiceInterface $service) { $self = parent::fromResponse($response, $service); - + $segments = Url::factory($response->getEffectiveUrl())->getPathSegments(); $self->name = end($segments); - + return $self; } @@ -85,10 +93,10 @@ public function getCdn() { if (!$this->isCdnEnabled()) { throw new Exceptions\CdnNotAvailableError( - 'Either this container is not CDN-enabled or the CDN is not available' + 'Either this container is not CDN-enabled or the CDN is not available' ); } - + return $this->cdn; } @@ -118,6 +126,7 @@ public function getBytesUsed() public function setCountQuota($value) { $this->metadata->setProperty('Quota-Count', $value); + return $this->saveMetadata($this->metadata->toArray()); } @@ -136,6 +145,7 @@ public function getCountQuota() public function setBytesQuota($value) { $this->metadata->setProperty('Quota-Bytes', $value); + return $this->saveMetadata($this->metadata->toArray()); } @@ -146,15 +156,16 @@ public function getBytesQuota() { return $this->metadata->getProperty('Quota-Bytes'); } - + public function delete($deleteObjects = false) { if ($deleteObjects === true) { - $this->deleteAllObjects(); + // Delegate to auxiliary method + return $this->deleteWithObjects(); } try { - $response = $this->getClient()->delete($this->getUrl())->send(); + return $this->getClient()->delete($this->getUrl())->send(); } catch (ClientErrorResponseException $e) { if ($e->getResponse()->getStatusCode() == 409) { throw new ContainerException(sprintf( @@ -167,6 +178,47 @@ public function delete($deleteObjects = false) } } + public function deleteWithObjects($secondsToWait = null) + { + // If container is empty, just delete it + $numObjects = (int) $this->retrieveMetadata()->getProperty('Object-Count'); + if (0 === $numObjects) { + return $this->delete(); + } + + // If timeout ($secondsToWait) is not specified by caller, + // try to estimate it based on number of objects in container + if (null === $secondsToWait) { + $secondsToWait = round($numObjects / 2); + } + + // Attempt to delete all objects and container + $endTime = time() + $secondsToWait; + $containerDeleted = false; + while ((time() < $endTime) && !$containerDeleted) { + $this->deleteAllObjects(); + try { + $response = $this->delete(); + $containerDeleted = true; + } catch (ContainerException $e) { + // Ignore exception and try again + } catch (ClientErrorResponseException $e) { + if ($e->getResponse()->getStatusCode() == 404) { + // Container has been deleted + $containerDeleted = true; + } else { + throw $e; + } + } + } + + if (!$containerDeleted) { + throw new ContainerException('Container and all its objects could not be deleted.'); + } + + return $response; + } + /** * Deletes all objects that this container currently contains. Useful when doing operations (like a delete) that * require an empty container first. @@ -175,42 +227,53 @@ public function delete($deleteObjects = false) */ public function deleteAllObjects() { - $requests = array(); - - $list = $this->objectList(); - - foreach ($list as $object) { - $requests[] = $this->getClient()->delete($object->getUrl()); + $paths = array(); + $objects = $this->objectList(); + foreach ($objects as $object) { + $paths[] = sprintf('/%s/%s', $this->getName(), $object->getName()); } + return $this->getService()->batchDelete($paths); + } - return $this->getClient()->send($requests); + /** + * Delete an object from the API. + * + * @param string $name The name of object you want to delete + * @throws \Guzzle\Http\Exception\BadResponseException When an error occurred + */ + public function deleteObject($name) + { + $this->getClient() + ->delete($this->getUrl($name)) + ->send(); } - + /** * Creates a Collection of objects in the container * * @param array $params associative array of parameter values. - * * account/tenant - The unique identifier of the account/tenant. - * * container- The unique identifier of the container. - * * limit (Optional) - The number limit of results. - * * marker (Optional) - Value of the marker, that the object names - * greater in value than are returned. - * * end_marker (Optional) - Value of the marker, that the object names - * less in value than are returned. - * * prefix (Optional) - Value of the prefix, which the returned object - * names begin with. - * * format (Optional) - Value of the serialized response format, either - * json or xml. - * * delimiter (Optional) - Value of the delimiter, that all the object - * names nested in the container are returned. - * @link http://api.openstack.org for a list of possible parameter - * names and values - * @return 'OpenCloud\Common\Collection + * * account/tenant - The unique identifier of the account/tenant. + * * container- The unique identifier of the container. + * * limit (Optional) - The number limit of results. + * * marker (Optional) - Value of the marker, that the object names + * greater in value than are returned. + * * end_marker (Optional) - Value of the marker, that the object names + * less in value than are returned. + * * prefix (Optional) - Value of the prefix, which the returned object + * names begin with. + * * format (Optional) - Value of the serialized response format, either + * json or xml. + * * delimiter (Optional) - Value of the delimiter, that all the object + * names nested in the container are returned. + * @link http://api.openstack.org for a list of possible parameter + * names and values + * @return \OpenCloud\Common\Collection * @throws ObjFetchError */ public function objectList(array $params = array()) { $params['format'] = 'json'; + return $this->getService()->resourceList('DataObject', $this->getUrl(null, $params), $this); } @@ -255,9 +318,9 @@ public function enableCdn($ttl = null) } /** - * Disables the containers CDN function. Note that the container will still + * Disables the containers CDN function. Note that the container will still * be available on the CDN until its TTL expires. - * + * * @return \Guzzle\Http\Message\Response */ public function disableCdn() @@ -273,22 +336,6 @@ public function refresh($id = null, $url = null) { $headers = $this->createRefreshRequest()->send()->getHeaders(); $this->setMetadata($headers, true); - - try { - if (null !== ($cdnService = $this->getService()->getCDNService())) { - $cdn = new CDNContainer($cdnService); - $cdn->setName($this->name); - - $response = $cdn->createRefreshRequest()->send(); - - if ($response->isSuccessful()) { - $this->cdn = $cdn; - $this->cdn->setMetadata($response->getHeaders(), true); - } - } else { - $this->cdn = null; - } - } catch (ClientErrorResponseException $e) {} } /** @@ -301,25 +348,25 @@ public function dataObject($info = null) { return new DataObject($this, $info); } - + /** - * Retrieve an object from the API. Apart from using the name as an - * identifier, you can also specify additional headers that will be used + * Retrieve an object from the API. Apart from using the name as an + * identifier, you can also specify additional headers that will be used * fpr a conditional GET request. These are - * + * * * `If-Match' * * `If-None-Match' * * `If-Modified-Since' * * `If-Unmodified-Since' - * * `Range' For example: + * * `Range' For example: * bytes=-5 would mean the last 5 bytes of the object * bytes=10-15 would mean 5 bytes after a 10 byte offset * bytes=32- would mean all dat after first 32 bytes - * + * * These are also documented in RFC 2616. - * + * * @param string $name - * @param array $headers + * @param array $headers * @return DataObject */ public function getObject($name, array $headers = array()) @@ -360,6 +407,32 @@ public function getPartialObject($name, array $headers = array()) ->setName($name); } + /** + * Check if an object exists inside a container. Uses {@see getPartialObject()} + * to save on bandwidth and time. + * + * @param $name Object name + * @return boolean True, if object exists in this container; false otherwise. + */ + public function objectExists($name) + { + try { + // Send HEAD request to check resource existence + $url = clone $this->getUrl(); + $url->addPath((string) $name); + $this->getClient()->head($url)->send(); + } catch (ClientErrorResponseException $e) { + // If a 404 was returned, then the object doesn't exist + if ($e->getResponse()->getStatusCode() === 404) { + return false; + } else { + throw $e; + } + } + + return true; + } + /** * Upload a single file to the API. * @@ -390,34 +463,35 @@ public function uploadObject($name, $data, array $headers = array()) * faster execution. This is a very useful procedure when you just have a bunch of unremarkable files to be * uploaded quickly. Each file must be under 5GB. * - * @param array $files With the following array structure: - * `name' Name that the file will be saved as in your container. Required. - * `path' Path to an existing file, OR - * `body' Either a string or stream representation of the file contents to be uploaded. + * @param array $files With the following array structure: + * `name' Name that the file will be saved as in your container. Required. + * `path' Path to an existing file, OR + * `body' Either a string or stream representation of the file contents to be uploaded. * @param array $headers Optional headers that will be sent with the request (useful for object metadata). + * @param string $returnType One of OpenCloud\ObjectStore\Enum\ReturnType::RESPONSE_ARRAY (to return an array of + * Guzzle\Http\Message\Response objects) or OpenCloud\ObjectStore\Enum\ReturnType::DATA_OBJECT_ARRAY + * (to return an array of OpenCloud\ObjectStore\Resource\DataObject objects). * * @throws \OpenCloud\Common\Exceptions\InvalidArgumentError - * @return \Guzzle\Http\Message\Response + * @return Guzzle\Http\Message\Response[] or OpenCloud\ObjectStore\Resource\DataObject[] depending on $returnType */ - public function uploadObjects(array $files, array $commonHeaders = array()) + public function uploadObjects(array $files, array $commonHeaders = array(), $returnType = ReturnType::RESPONSE_ARRAY) { $requests = $entities = array(); foreach ($files as $entity) { - if (empty($entity['name'])) { - throw new Exceptions\InvalidArgumentError('You must provide a name.'); - } - + throw new Exceptions\InvalidArgumentError('You must provide a name.'); + } + if (!empty($entity['path']) && file_exists($entity['path'])) { - $body = fopen($entity['path'], 'r+'); - - } elseif (!empty($entity['body'])) { - $body = $entity['body']; - } else { - throw new Exceptions\InvalidArgumentError('You must provide either a readable path or a body'); - } - + $body = fopen($entity['path'], 'r+'); + } elseif (!empty($entity['body'])) { + $body = $entity['body']; + } else { + throw new Exceptions\InvalidArgumentError('You must provide either a readable path or a body'); + } + $entityBody = $entities[] = EntityBody::factory($body); // @codeCoverageIgnoreStart @@ -440,11 +514,22 @@ public function uploadObjects(array $files, array $commonHeaders = array()) $responses = $this->getClient()->send($requests); - foreach ($entities as $entity) { - $entity->close(); + if (ReturnType::RESPONSE_ARRAY === $returnType) { + foreach ($entities as $entity) { + $entity->close(); + } + return $responses; + } else { + // Convert responses to DataObjects before returning + $dataObjects = array(); + foreach ($responses as $index => $response) { + $dataObjects[] = $this->dataObject() + ->populateFromResponse($response) + ->setName($files[$index]['name']) + ->setContent($entities[$index]); + } + return $dataObjects; } - - return $responses; } /** @@ -471,13 +556,13 @@ public function setupObjectTransfer(array $options = array()) } else { throw new Exceptions\InvalidArgumentError('You must provide either a readable path or a body'); } - + // Build upload $transfer = TransferBuilder::newInstance() ->setOption('objectName', $options['name']) ->setEntityBody(EntityBody::factory($body)) ->setContainer($this); - + // Add extra options if (!empty($options['metadata'])) { $transfer->setOption('metadata', $options['metadata']); @@ -498,16 +583,41 @@ public function setupObjectTransfer(array $options = array()) /** * Upload the contents of a local directory to a remote container, effectively syncing them. * - * @param $path The local path to the directory. + * @param string $path The local path to the directory. + * @param string $targetDir The path (or pseudo-directory) that all files will be nested in. */ - public function uploadDirectory($path) + public function uploadDirectory($path, $targetDir = null) { - $sync = DirectorySync::factory($path, $this); + $sync = DirectorySync::factory($path, $this, $targetDir); $sync->execute(); } public function isCdnEnabled() { + // If CDN object is not already populated, try to populate it. + if (null === $this->cdn) { + $this->refreshCdnObject(); + } return ($this->cdn instanceof CDNContainer) && $this->cdn->isCdnEnabled(); } -} \ No newline at end of file + + protected function refreshCdnObject() + { + try { + if (null !== ($cdnService = $this->getService()->getCDNService())) { + $cdn = new CDNContainer($cdnService); + $cdn->setName($this->name); + + $response = $cdn->createRefreshRequest()->send(); + + if ($response->isSuccessful()) { + $this->cdn = $cdn; + $this->cdn->setMetadata($response->getHeaders(), true); + } + } else { + $this->cdn = null; + } + } catch (ClientErrorResponseException $e) { + } + } +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Resource/ContainerMetadata.php b/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Resource/ContainerMetadata.php index 942ab3c9e..7e2e76822 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Resource/ContainerMetadata.php +++ b/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Resource/ContainerMetadata.php @@ -1,13 +1,22 @@ - * @author Glen Campbell + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\ObjectStore\Resource; -class ContainerMetadata extends \OpenCloud\Common\Metadata {} \ No newline at end of file +class ContainerMetadata extends \OpenCloud\Common\Metadata +{ +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Resource/DataObject.php b/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Resource/DataObject.php index c7f431b99..5eb3c085e 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Resource/DataObject.php +++ b/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Resource/DataObject.php @@ -1,11 +1,18 @@ - * @author Glen Campbell + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\ObjectStore\Resource; @@ -14,15 +21,16 @@ use Guzzle\Http\Message\Response; use Guzzle\Http\Url; use OpenCloud\Common\Constants\Header as HeaderConst; -use OpenCloud\Common\Lang; use OpenCloud\Common\Exceptions; +use OpenCloud\Common\Lang; use OpenCloud\ObjectStore\Constants\UrlType; +use OpenCloud\ObjectStore\Exception\ObjectNotEmptyException; /** - * Objects are the basic storage entities in Cloud Files. They represent the - * files and their optional metadata you upload to the system. When you upload - * objects to Cloud Files, the data is stored as-is (without compression or - * encryption) and consists of a location (container), the object's name, and + * Objects are the basic storage entities in Cloud Files. They represent the + * files and their optional metadata you upload to the system. When you upload + * objects to Cloud Files, the data is stored as-is (without compression or + * encryption) and consists of a location (container), the object's name, and * any metadata you assign consisting of key/value pairs. */ class DataObject extends AbstractResource @@ -38,9 +46,9 @@ class DataObject extends AbstractResource * @var The file name of the object */ protected $name; - + /** - * @var EntityBody + * @var EntityBody */ protected $content; @@ -69,6 +77,11 @@ class DataObject extends AbstractResource * @var string Etag. */ protected $etag; + + /** + * @var string Manifest. Can be null so we use false to mean unset. + */ + protected $manifest = false; /** * Also need to set Container parent and handle pseudo-directories. @@ -82,10 +95,11 @@ public function __construct(Container $container, $data = null) $this->setContainer($container); parent::__construct($container->getService()); - + // For pseudo-directories, we need to ensure the name is set if (!empty($data->subdir)) { $this->setName($data->subdir)->setDirectory(true); + return; } @@ -131,7 +145,9 @@ public function populateFromResponse(Response $response) ->setContentType((string) $headers[HeaderConst::CONTENT_TYPE]) ->setLastModified((string) $headers[HeaderConst::LAST_MODIFIED]) ->setContentLength((string) $headers[HeaderConst::CONTENT_LENGTH]) - ->setEtag((string) $headers[HeaderConst::ETAG]); + ->setEtag((string) $headers[HeaderConst::ETAG]) + // do not cast to a string to allow for null (i.e. no header) + ->setManifest($headers[HeaderConst::X_OBJECT_MANIFEST]); } public function refresh() @@ -150,6 +166,7 @@ public function refresh() public function setContainer(Container $container) { $this->container = $container; + return $this; } @@ -168,6 +185,7 @@ public function getContainer() public function setName($name) { $this->name = $name; + return $this; } @@ -186,6 +204,7 @@ public function getName() public function setDirectory($directory) { $this->directory = $directory; + return $this; } @@ -196,7 +215,7 @@ public function getDirectory() { return $this->directory; } - + /** * @return bool Is this data object a pseudo-directory? */ @@ -214,6 +233,7 @@ public function setContent($content) $this->etag = null; $this->contentType = null; $this->content = EntityBody::factory($content); + return $this; } @@ -232,6 +252,7 @@ public function getContent() public function setContentType($contentType) { $this->contentType = $contentType; + return $this; } @@ -240,21 +261,22 @@ public function setContentType($contentType) */ public function getContentType() { - return $this->contentType ?: $this->content->getContentType(); + return $this->contentType ? : $this->content->getContentType(); } /** - * @param $contentType int + * @param $contentLength mixed * @return $this */ public function setContentLength($contentLength) { $this->contentLength = $contentLength; + return $this; } /** - * @return int + * @return mixed */ public function getContentLength() { @@ -268,6 +290,7 @@ public function getContentLength() public function setEtag($etag) { $this->etag = $etag; + return $this; } @@ -276,12 +299,33 @@ public function setEtag($etag) */ public function getEtag() { - return $this->etag ?: $this->content->getContentMd5(); + return $this->etag ? : $this->content->getContentMd5(); + } + + /** + * @param string $manifest Path (`container/object') to set as the value to X-Object-Manifest + * @return $this + */ + protected function setManifest($manifest) + { + $this->manifest = $manifest; + + return $this; + } + + /** + * @return null|string Path (`container/object') from X-Object-Manifest header or null if the header does not exist + */ + public function getManifest() + { + // only make a request if manifest has not been set (is false) + return $this->manifest !== false ? $this->manifest : $this->getManifestHeader(); } public function setLastModified($lastModified) { $this->lastModified = $lastModified; + return $this; } @@ -311,10 +355,11 @@ public function update($params = array()) // merge specific properties with metadata $metadata += array( - HeaderConst::CONTENT_TYPE => $this->contentType, - HeaderConst::LAST_MODIFIED => $this->lastModified, - HeaderConst::CONTENT_LENGTH => $this->contentLength, - HeaderConst::ETAG => $this->etag + HeaderConst::CONTENT_TYPE => $this->contentType, + HeaderConst::LAST_MODIFIED => $this->lastModified, + HeaderConst::CONTENT_LENGTH => $this->contentLength, + HeaderConst::ETAG => $this->etag, + HeaderConst::X_OBJECT_MANIFEST => $this->manifest ); return $this->container->uploadObject($this->name, $this->content, $metadata); @@ -338,20 +383,86 @@ public function delete($params = array()) { return $this->getService()->getClient()->delete($this->getUrl())->send(); } + + /** + * Create a symlink to another named object from this object. Requires this object to be empty. + * + * @param string $destination Path (`container/object') of other object to symlink this object to + * @return \Guzzle\Http\Message\Response The response + * @throws \OpenCloud\Common\Exceptions\NoNameError if a destination name is not provided + * @throws \OpenCloud\ObjectStore\Exception\ObjectNotEmptyException if $this is not an empty object + */ + public function createSymlinkTo($destination) + { + if (!$this->name) { + throw new Exceptions\NoNameError(Lang::translate('Object has no name')); + } + + if ($this->getContentLength()) { + throw new ObjectNotEmptyException($this->getContainer()->getName() . '/' . $this->getName()); + } + + $response = $this->getService() + ->getClient() + ->createRequest('PUT', $this->getUrl(), array( + HeaderConst::X_OBJECT_MANIFEST => (string) $destination + )) + ->send(); + + if ($response->getStatusCode() == 201) { + $this->setManifest($source); + } + + return $response; + } + + /** + * Create a symlink to this object from another named object. Requires the other object to either not exist or be empty. + * + * @param string $source Path (`container/object') of other object to symlink this object from + * @return DataObject The symlinked object + * @throws \OpenCloud\Common\Exceptions\NoNameError if a source name is not provided + * @throws \OpenCloud\ObjectStore\Exception\ObjectNotEmptyException if object already exists and is not empty + */ + public function createSymlinkFrom($source) + { + if (!strlen($source)) { + throw new Exceptions\NoNameError(Lang::translate('Object has no name')); + } + + // Use ltrim to remove leading slash from source + list($containerName, $resourceName) = explode("/", ltrim($source, '/'), 2); + $container = $this->getService()->getContainer($containerName); + + if ($container->objectExists($resourceName)) { + $object = $container->getPartialObject($source); + if ($object->getContentLength() > 0) { + throw new ObjectNotEmptyException($source); + } + } + + return $container->uploadObject($resourceName, 'data', array( + HeaderConst::X_OBJECT_MANIFEST => (string) $this->getUrl() + )); + } /** * Get a temporary URL for this object. * * @link http://docs.rackspace.com/files/api/v1/cf-devguide/content/TempURL-d1a4450.html * - * @param $expires Expiration time in seconds - * @param $method What method can use this URL? (`GET' or `PUT') + * @param int $expires Expiration time in seconds + * @param string $method What method can use this URL? (`GET' or `PUT') + * @param bool $forcePublicUrl If set to TRUE, a public URL will always be used. The default is to use whatever + * URL type the user has set for the main service. + * * @return string + * * @throws \OpenCloud\Common\Exceptions\InvalidArgumentError * @throws \OpenCloud\Common\Exceptions\ObjectError * */ - public function getTemporaryUrl($expires, $method) + public function getTemporaryUrl($expires, $method, $forcePublicUrl = false) { $method = strtoupper($method); $expiry = time() + (int) $expires; @@ -363,14 +474,18 @@ public function getTemporaryUrl($expires, $method) $method )); } - + // @codeCoverageIgnoreStart if (!($secret = $this->getService()->getAccount()->getTempUrlSecret())) { throw new Exceptions\ObjectError('Cannot produce temporary URL without an account secret.'); } // @codeCoverageIgnoreEnd - $url = $this->getUrl(); + $url = $this->getUrl(); + if ($forcePublicUrl === true) { + $url->setHost($this->getService()->getEndpoint()->getPublicUrl()->getHost()); + } + $urlPath = urldecode($url->getPath()); $body = sprintf("%s\n%d\n%s", $method, $expiry, $urlPath); $hash = hash_hmac('sha1', $body, $secret); @@ -408,7 +523,7 @@ public function purge($email = null) public function getPublicUrl($type = UrlType::CDN) { $cdn = $this->container->getCdn(); - + switch ($type) { case UrlType::CDN: $uri = $cdn->getCdnUri(); @@ -420,16 +535,34 @@ public function getPublicUrl($type = UrlType::CDN) $uri = $cdn->getCdnStreamingUri(); break; case UrlType::IOS_STREAMING: - $uri = $cdn->getIosStreamingUri(); - break; + $uri = $cdn->getIosStreamingUri(); + break; } - + return (isset($uri)) ? Url::factory($uri)->addPath($this->name) : false; } protected static function headerIsValidMetadata($header) { $pattern = sprintf('#^%s-%s-Meta-#i', self::GLOBAL_METADATA_PREFIX, self::METADATA_LABEL); + return preg_match($pattern, $header); } -} \ No newline at end of file + + /** + * @return null|string + */ + protected function getManifestHeader() + { + $response = $this->getService() + ->getClient() + ->head($this->getUrl()) + ->send(); + + $manifest = $response->getHeader(HeaderConst::X_OBJECT_MANIFEST); + + $this->setManifest($manifest); + + return $manifest; + } +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Service.php b/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Service.php index 430374e61..0079027b0 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Service.php +++ b/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Service.php @@ -1,35 +1,43 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\ObjectStore; use Guzzle\Http\EntityBody; -use Guzzle\Http\Url; use OpenCloud\Common\Constants\Header; use OpenCloud\Common\Constants\Mime; use OpenCloud\Common\Exceptions; use OpenCloud\Common\Exceptions\InvalidArgumentError; use OpenCloud\Common\Http\Client; use OpenCloud\Common\Http\Message\Formatter; +use OpenCloud\Common\Log\Logger; use OpenCloud\Common\Service\ServiceBuilder; -use OpenCloud\ObjectStore\Resource\Container; use OpenCloud\ObjectStore\Constants\UrlType; +use OpenCloud\ObjectStore\Resource\Container; use OpenCloud\ObjectStore\Upload\ContainerMigration; /** * The ObjectStore (Cloud Files) service. */ -class Service extends AbstractService +class Service extends AbstractService { const DEFAULT_NAME = 'cloudFiles'; const DEFAULT_TYPE = 'object-store'; + const BATCH_DELETE_MAX = 10000; /** * This holds the associated CDN service (for Rackspace public cloud) @@ -46,56 +54,81 @@ public function __construct(Client $client, $type = null, $name = null, $region $this->cdnService = ServiceBuilder::factory($client, 'OpenCloud\ObjectStore\CDNService', array( 'region' => $region )); - } catch (Exceptions\EndpointError $e) {} + } catch (Exceptions\EndpointError $e) { + } } /** - * @return CDNService + * Return the CDN version of the ObjectStore service. + * + * @return CDNService CDN version of the ObjectStore service */ - public function getCdnService() + public function getCdnService() { return $this->cdnService; } /** - * @param $data - * @return Container + * List all available containers. + * + * @param array $filter Array of filter options such as: + * + * * `limit`: number of results to limit the list to. Optional. + * * `marker`: name of container after which to start the list. Optional. + * * `end_marker`: name of container before which to end the list. Optional. + * @return \OpenCloud\Common\Collection\PaginatedIterator Iterator to list of containers + */ + public function listContainers(array $filter = array()) + { + $filter['format'] = 'json'; + return $this->resourceList('Container', $this->getUrl(null, $filter), $this); + } + + /** + * Return a new or existing (if name is specified) container. + * + * @param \stdClass $data Data to initialize container. Optional. + * @return Container Container */ public function getContainer($data = null) { + if (is_string($data) || is_numeric($data)) { + $this->checkContainerName($data); + } + return new Container($this, $data); } /** * Create a container for this service. * - * @param $name The name of the container + * @param string $name The name of the container * @param array $metadata Additional (optional) metadata to associate with the container - * @return bool|static + * @return bool|Container Newly-created Container upon success; false, otherwise */ public function createContainer($name, array $metadata = array()) { $this->checkContainerName($name); - + $containerHeaders = Container::stockHeaders($metadata); - + $response = $this->getClient() ->put($this->getUrl($name), $containerHeaders) ->send(); - + if ($response->getStatusCode() == 201) { return Container::fromResponse($response, $this); } - + return false; } /** * Check the validity of a potential container name. * - * @param $name - * @return bool - * @throws \OpenCloud\Common\Exceptions\InvalidArgumentError + * @param string $name Name of container + * @return bool True if container name is valid + * @throws \OpenCloud\Common\Exceptions\InvalidArgumentError if container name is invalid */ public function checkContainerName($name) { @@ -110,7 +143,7 @@ public function checkContainerName($name) if (strlen($name) > self::MAX_CONTAINER_NAME_LENGTH) { $error = 'Container name is too long'; } - + if (isset($error)) { throw new InvalidArgumentError($error); } @@ -124,23 +157,23 @@ public function checkContainerName($name) * will be ignored. You can create up to 1,000 new containers per extraction request. Also note that only regular * files will be uploaded. Empty directories, symlinks, and so on, will not be uploaded. * - * @param $path The path to the archive being extracted - * @param $archive The contents of the archive (either string or stream) + * @param string $path The path to the archive being extracted + * @param string|stream $archive The contents of the archive (either string or stream) * @param string $archiveType The type of archive you're using {@see \OpenCloud\ObjectStore\Constants\UrlType} - * @return \Guzzle\Http\Message\Response - * @throws Exception\BulkOperationException - * @throws \OpenCloud\Common\Exceptions\InvalidArgumentError + * @return \Guzzle\Http\Message\Response HTTP response from API + * @throws \OpenCloud\Common\Exceptions\InvalidArgumentError if specifed `$archiveType` is invalid + * @throws Exception\BulkOperationException if there are errors with the bulk extract */ public function bulkExtract($path = '', $archive, $archiveType = UrlType::TAR_GZ) { $entity = EntityBody::factory($archive); - + $acceptableTypes = array( UrlType::TAR, UrlType::TAR_GZ, UrlType::TAR_BZ2 ); - + if (!in_array($archiveType, $acceptableTypes)) { throw new InvalidArgumentError(sprintf( 'The archive type must be one of the following: [%s]. You provided [%s].', @@ -148,33 +181,74 @@ public function bulkExtract($path = '', $archive, $archiveType = UrlType::TAR_GZ print_r($archiveType, true) )); } - + $url = $this->getUrl()->addPath($path)->setQuery(array('extract-archive' => $archiveType)); $response = $this->getClient()->put($url, array(Header::CONTENT_TYPE => ''), $entity)->send(); - + $body = Formatter::decode($response); if (!empty($body->Errors)) { throw new Exception\BulkOperationException((array) $body->Errors); } - + return $response; } /** - * This method will delete multiple objects or containers from their account with a single request. + * @deprecated Please use {@see batchDelete()} instead. + */ + public function bulkDelete(array $paths) + { + $this->getLogger()->warning(Logger::deprecated(__METHOD__, '::batchDelete()')); + + return $this->executeBatchDeleteRequest($paths); + } + + /** + * Batch delete will delete an array of object paths. By default, + * the API will only accept a maximum of 10,000 object deletions + * per request - so for arrays that exceed this size, it is chunked + * and sent as individual requests. * - * @param array $paths A two-dimensional array of paths: - * array('container_a/file_1', 'container_b/file_78', 'container_c/file_40582') + * @param array $paths The objects you want to delete. Each path needs + * be formatted as `/{containerName}/{objectName}`. If + * you are deleting `object_1` and `object_2` from the + * `photos_container`, the array will be: + * + * array( + * '/photos_container/object_1', + * '/photos_container/object_2' + * ) + * + * @return array[Guzzle\Http\Message\Response] HTTP responses from the API + * @throws Exception\BulkOperationException if the bulk delete operation fails + */ + public function batchDelete(array $paths) + { + $chunks = array_chunk($paths, self::BATCH_DELETE_MAX); + + $responses = array(); + + foreach ($chunks as $chunk) { + $responses[] = $this->executeBatchDeleteRequest($chunk); + } + + return $responses; + } + + /** + * Internal method for dispatching single batch delete requests. + * + * @param array $paths * @return \Guzzle\Http\Message\Response * @throws Exception\BulkOperationException */ - public function bulkDelete(array $paths) + private function executeBatchDeleteRequest(array $paths) { $entity = EntityBody::factory(implode(PHP_EOL, $paths)); - + $url = $this->getUrl()->setQuery(array('bulk-delete' => true)); - + $response = $this->getClient() ->delete($url, array(Header::CONTENT_TYPE => Mime::TEXT), $entity) ->send(); @@ -184,8 +258,9 @@ public function bulkDelete(array $paths) if (!empty($body->Errors)) { throw new Exception\BulkOperationException((array) $body->Errors); } - } catch (Exceptions\JsonError $e) {} - + } catch (Exceptions\JsonError $e) { + } + return $response; } @@ -194,7 +269,12 @@ public function bulkDelete(array $paths) * * @param Container $old Where you're moving files from * @param Container $new Where you're moving files to - * @return array Of PUT responses + * @param array $options Options to configure the migration. Optional. Available options are: + * + * * `read.batchLimit`: Number of files to read at a time from `$old` container. Optional; default = 1000. + * * `write.batchLimit`: Number of files to write at a time to `$new` container. Optional; default = 1000. + * * `read.pageLimit`: Number of filenames to read at a time from `$old` container. Optional; default = 10000. + * @return array[Guzzle\Http\Message\Response] HTTP responses from the API */ public function migrateContainer(Container $old, Container $new, array $options = array()) { @@ -202,4 +282,4 @@ public function migrateContainer(Container $old, Container $new, array $options return $migration->transfer(); } -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Upload/AbstractTransfer.php b/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Upload/AbstractTransfer.php index 3eebeb89c..4d794d36f 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Upload/AbstractTransfer.php +++ b/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Upload/AbstractTransfer.php @@ -1,19 +1,26 @@ - * @author Glen Campbell + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\ObjectStore\Upload; use Exception; -use OpenCloud\Common\Http\Client; use Guzzle\Http\EntityBody; use OpenCloud\Common\Exceptions\RuntimeException; +use OpenCloud\Common\Http\Client; use OpenCloud\ObjectStore\Exception\UploadException; /** @@ -25,12 +32,12 @@ class AbstractTransfer * Minimum chunk size is 1MB. */ const MIN_PART_SIZE = 1048576; - + /** * Maximum chunk size is 5GB. */ const MAX_PART_SIZE = 5368709120; - + /** * Default chunk size is 1GB. */ @@ -88,6 +95,7 @@ public static function newInstance() public function setClient(Client $client) { $this->client = $client; + return $this; } @@ -98,6 +106,7 @@ public function setClient(Client $client) public function setEntityBody(EntityBody $entityBody) { $this->entityBody = $entityBody; + return $this; } @@ -108,6 +117,7 @@ public function setEntityBody(EntityBody $entityBody) public function setTransferState(TransferState $transferState) { $this->transferState = $transferState; + return $this; } @@ -126,6 +136,7 @@ public function getOptions() public function setOptions($options) { $this->options = $options; + return $this; } @@ -137,6 +148,7 @@ public function setOptions($options) public function setOption($option, $value) { $this->options[$option] = $value; + return $this; } @@ -150,9 +162,9 @@ public function getPartSize() */ public function setup() { - $this->options = array_merge($this->defaultOptions, $this->options); + $this->options = array_merge($this->defaultOptions, $this->options); $this->partSize = $this->validatePartSize(); - + return $this; } @@ -164,9 +176,10 @@ public function setup() protected function validatePartSize() { $min = min($this->options['partSize'], self::MAX_PART_SIZE); + return max($min, self::MIN_PART_SIZE); } - + /** * Initiates the upload procedure. * @@ -190,7 +203,7 @@ public function upload() return $response; } - + /** * With large uploads, you must create a manifest file. Although each segment or TransferPart remains * individually addressable, the manifest file serves as the unified file (i.e. the 5GB download) which, when @@ -203,7 +216,7 @@ public function upload() private function createManifest() { $parts = array(); - + foreach ($this->transferState as $part) { $parts[] = (object) array( 'path' => $part->getPath(), @@ -211,20 +224,19 @@ private function createManifest() 'size_bytes' => $part->getContentLength() ); } - + $headers = array( 'Content-Length' => 0, - 'X-Object-Manifest' => sprintf('%s/%s/%s/', + 'X-Object-Manifest' => sprintf('%s/%s/%s/', $this->options['containerName'], - $this->options['objectName'], + $this->options['objectName'], $this->options['prefix'] ) ); - + $url = clone $this->options['containerUrl']; $url->addPath($this->options['objectName']); - + return $this->client->put($url, $headers)->send(); } - -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Upload/ConcurrentTransfer.php b/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Upload/ConcurrentTransfer.php index 0b42da848..d0a58f736 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Upload/ConcurrentTransfer.php +++ b/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Upload/ConcurrentTransfer.php @@ -1,17 +1,24 @@ - * @author Glen Campbell + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\ObjectStore\Upload; -use Guzzle\Http\ReadLimitEntityBody; use Guzzle\Http\EntityBody; +use Guzzle\Http\ReadLimitEntityBody; /** * A transfer type which executes in a concurrent fashion, i.e. with multiple workers uploading at once. Each worker is @@ -27,19 +34,17 @@ public function transfer() $totalParts = (int) ceil($this->entityBody->getContentLength() / $this->partSize); $workers = min($totalParts, $this->options['concurrency']); $parts = $this->collectParts($workers); - + while ($this->transferState->count() < $totalParts) { - $completedParts = $this->transferState->count(); $requests = array(); - + // Iterate over number of workers until total completed parts is what we need it to be for ($i = 0; $i < $workers && ($completedParts + $i) < $totalParts; $i++) { - // Offset is the current pointer multiplied by the standard chunk length $offset = ($completedParts + $i) * $this->partSize; $parts[$i]->setOffset($offset); - + // If this segment is empty (i.e. buffering a half-full chunk), break the iteration if ($parts[$i]->getContentLength() == 0) { break; @@ -47,9 +52,9 @@ public function transfer() // Add this to the request queue for later processing $requests[] = TransferPart::createRequest( - $parts[$i], - $this->transferState->count() + $i + 1, - $this->client, + $parts[$i], + $this->transferState->count() + $i + 1, + $this->client, $this->options ); } @@ -73,15 +78,14 @@ public function transfer() private function collectParts($workers) { $uri = $this->entityBody->getUri(); - + $array = array(new ReadLimitEntityBody($this->entityBody, $this->partSize)); - + for ($i = 1; $i < $workers; $i++) { - // Need to create a fresh EntityBody, otherwise you'll get weird 408 responses + // Need to create a fresh EntityBody, otherwise you'll get weird 408 responses $array[] = new ReadLimitEntityBody(new EntityBody(fopen($uri, 'r')), $this->partSize); } return $array; } - -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Upload/ConsecutiveTransfer.php b/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Upload/ConsecutiveTransfer.php index 781514159..50196802d 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Upload/ConsecutiveTransfer.php +++ b/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Upload/ConsecutiveTransfer.php @@ -1,11 +1,18 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\ObjectStore\Upload; @@ -22,11 +29,9 @@ */ class ConsecutiveTransfer extends AbstractTransfer { - public function transfer() { while (!$this->entityBody->isConsumed()) { - if ($this->entityBody->getContentLength() && $this->entityBody->isSeekable()) { // Stream directly from the data $body = new ReadLimitEntityBody($this->entityBody, $this->partSize, $this->entityBody->ftell()); @@ -46,16 +51,15 @@ public function transfer() } $request = TransferPart::createRequest( - $body, + $body, $this->transferState->count() + 1, - $this->client, + $this->client, $this->options ); - + $response = $request->send(); $this->transferState->addPart(TransferPart::fromResponse($response)); } } - -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Upload/ContainerMigration.php b/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Upload/ContainerMigration.php index e50526cc2..ebbfbb2cf 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Upload/ContainerMigration.php +++ b/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Upload/ContainerMigration.php @@ -1,10 +1,18 @@ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\ObjectStore\Upload; @@ -44,9 +52,9 @@ class ContainerMigration ); /** - * @param Container $old Source container - * @param Container $new Target container - * @param array $options Options that configure process + * @param Container $old Source container + * @param Container $new Target container + * @param array $options Options that configure process * @return ContainerMigration */ public static function factory(Container $old, Container $new, array $options = array()) @@ -164,6 +172,7 @@ protected function enqueueGetRequests() protected function sendGetRequests() { $this->enqueueGetRequests(); + return $this->readQueue->flush(); } @@ -208,4 +217,4 @@ public function transfer() return $this->writeQueue->flush(); } -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Upload/DirectorySync.php b/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Upload/DirectorySync.php index 213e19683..fc30b9968 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Upload/DirectorySync.php +++ b/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Upload/DirectorySync.php @@ -1,10 +1,18 @@ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\ObjectStore\Upload; @@ -13,7 +21,6 @@ use Guzzle\Http\EntityBody; use OpenCloud\Common\Collection\ResourceIterator; use OpenCloud\Common\Exceptions\InvalidArgumentError; -use OpenCloud\Common\Exceptions\IOError; use OpenCloud\ObjectStore\Resource\Container; /** @@ -24,32 +31,34 @@ */ class DirectorySync { - /** - * @var string The path to the directory you're syncing. - */ + /** @var string The path to the directory you're syncing. */ private $basePath; - /** - * @var ResourceIterator A collection of remote files in Swift. - */ + + /** @var ResourceIterator A collection of remote files in Swift. */ private $remoteFiles; - /** - * @var AbstractContainer The Container object you are syncing. - */ + + /** @var AbstractContainer The Container object you are syncing. */ private $container; + /** @var string */ + private $targetDir; + /** * Basic factory method to instantiate a new DirectorySync object with all the appropriate properties. * - * @param $path The local path + * @param string $path The local path * @param Container $container The container you're syncing + * @param string $targetDir The path (or pseudo-directory) that the files will be nested in + * * @return DirectorySync */ - public static function factory($path, Container $container) + public static function factory($path, Container $container, $targetDir = null) { $transfer = new self(); $transfer->setBasePath($path); $transfer->setContainer($container); $transfer->setRemoteFiles($container->objectList()); + $transfer->setTargetDir($targetDir); return $transfer; } @@ -83,6 +92,14 @@ public function setContainer(Container $container) $this->container = $container; } + /** + * @param string $dir The target path that all files will be nested in. By default, the files will not be nested. + */ + public function setTargetDir($dir) + { + $this->targetDir = rtrim($dir, '/'); + } + /** * Execute the sync process. This will collect all the remote files from the API and do a comparison. There are * four scenarios that need to be dealt with: @@ -105,8 +122,9 @@ public function execute() // Handle PUT requests (create/update files) foreach ($localFiles as $filename) { + $remoteFilename = $this->targetDir ? $this->targetDir . '/' . $filename : $filename; - $callback = $this->getCallback($filename); + $callback = $this->getCallback($remoteFilename); $filePath = rtrim($this->basePath, '/') . '/' . $filename; if (!is_readable($filePath)) { @@ -124,11 +142,10 @@ public function execute() $entityBody ); } - } else { // upload new file $url = clone $this->container->getUrl(); - $url->addPath($filename); + $url->addPath($remoteFilename); $requests[] = $this->container->getClient()->put($url, array(), $entityBody); } @@ -206,12 +223,13 @@ private function trimFilename($file) private function getCallback($name) { $name = trim($name, '/'); - return function($remoteFile) use ($name) { + + return function ($remoteFile) use ($name) { if ($remoteFile->getName() == $name) { return true; } + return false; }; } - -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Upload/TransferBuilder.php b/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Upload/TransferBuilder.php index 1dc39aee4..3af55a369 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Upload/TransferBuilder.php +++ b/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Upload/TransferBuilder.php @@ -1,16 +1,24 @@ + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\ObjectStore\Upload; -use OpenCloud\Common\Exceptions\InvalidArgumentError; use Guzzle\Http\EntityBody; +use OpenCloud\Common\Exceptions\InvalidArgumentError; use OpenCloud\ObjectStore\Resource\Container; /** @@ -40,25 +48,26 @@ public static function newInstance() { return new self(); } - + /** * @param type $options Available configuration options: - * + * * * `concurrency' The number of concurrent workers. * * `partSize' The size, in bytes, for the chunk * * `doPartChecksum' Enable or disable MD5 checksum in request (ETag) - * + * * If you are uploading FooBar, its chunks will have the following naming structure: - * + * * FooBar/1 * FooBar/2 * FooBar/3 - * + * * @return \OpenCloud\ObjectStore\Upload\UploadBuilder */ public function setOptions($options) { $this->options = $options; + return $this; } @@ -70,6 +79,7 @@ public function setOptions($options) public function setOption($key, $value) { $this->options[$key] = $value; + return $this; } @@ -80,6 +90,7 @@ public function setOption($key, $value) public function setContainer(Container $container) { $this->container = $container; + return $this; } @@ -90,6 +101,7 @@ public function setContainer(Container $container) public function setEntityBody(EntityBody $entityBody) { $this->entityBody = $entityBody; + return $this; } @@ -105,15 +117,15 @@ public function build() if (!$this->container || !$this->entityBody || !$this->options['objectName']) { throw new InvalidArgumentError('A container, entity body and object name must be set'); } - + // Create TransferState object for later use $transferState = TransferState::factory(); - - // Instantiate Concurrent-/ConsecutiveTransfer - $transferClass = isset($this->options['concurrency']) && $this->options['concurrency'] > 1 - ? __NAMESPACE__ . '\\ConcurrentTransfer' + + // Instantiate Concurrent-/ConsecutiveTransfer + $transferClass = isset($this->options['concurrency']) && $this->options['concurrency'] > 1 + ? __NAMESPACE__ . '\\ConcurrentTransfer' : __NAMESPACE__ . '\\ConsecutiveTransfer'; - + return $transferClass::newInstance() ->setClient($this->container->getClient()) ->setEntityBody($this->entityBody) @@ -123,5 +135,4 @@ public function build() ->setOption('containerUrl', $this->container->getUrl()) ->setup(); } - -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Upload/TransferPart.php b/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Upload/TransferPart.php index 9b770a7a4..3da3f0162 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Upload/TransferPart.php +++ b/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Upload/TransferPart.php @@ -1,11 +1,18 @@ - * @author Glen Campbell + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\ObjectStore\Upload; @@ -13,7 +20,6 @@ use Guzzle\Http\Message\Response; use Guzzle\Http\Url; use OpenCloud\Common\Constants\Header; -use OpenCloud\Common\Constants\Mime; /** * Represents an individual part of the EntityBody being uploaded. @@ -26,17 +32,17 @@ class TransferPart * @var int Its position in the upload queue. */ protected $partNumber; - + /** * @var string This upload's ETag checksum. */ protected $eTag; - + /** * @var int The length of this upload in bytes. */ protected $contentLength; - + /** * @var string The API path of this upload. */ @@ -49,6 +55,7 @@ class TransferPart public function setContentLength($contentLength) { $this->contentLength = $contentLength; + return $this; } @@ -67,6 +74,7 @@ public function getContentLength() public function setETag($etag) { $this->etag = $etag; + return $this; } @@ -85,6 +93,7 @@ public function getETag() public function setPartNumber($partNumber) { $this->partNumber = $partNumber; + return $this; } @@ -103,6 +112,7 @@ public function getPartNumber() public function setPath($path) { $this->path = $path; + return $this; } @@ -113,10 +123,10 @@ public function getPath() { return $this->path; } - + /** * Create the request needed for this upload to the API. - * + * * @param EntityBody $part The entity body being uploaded * @param int $number Its number/position, needed for name * @param OpenStack $client Client responsible for issuing requests @@ -126,24 +136,24 @@ public function getPath() public static function createRequest($part, $number, $client, $options) { $name = sprintf('%s/%s/%d', $options['objectName'], $options['prefix'], $number); - $url = clone $options['containerUrl']; + $url = clone $options['containerUrl']; $url->addPath($name); $headers = array( Header::CONTENT_LENGTH => $part->getContentLength(), Header::CONTENT_TYPE => $part->getContentType() - ); - + ); + if ($options['doPartChecksum'] === true) { $headers['ETag'] = $part->getContentMd5(); } - + $request = $client->put($url, $headers, $part); - + if (isset($options['progress'])) { $request->getCurlOptions()->add('progress', true); if (is_callable($options['progress'])) { - $request->getCurlOptions()->add('progressCallback', $options['progress']); + $request->getCurlOptions()->add('progressCallback', $options['progress']); } } @@ -160,15 +170,14 @@ public static function createRequest($part, $number, $client, $options) public static function fromResponse(Response $response, $partNumber = 1) { $responseUri = Url::factory($response->getEffectiveUrl()); - + $object = new self(); - + $object->setPartNumber($partNumber) ->setContentLength($response->getHeader(Header::CONTENT_LENGTH)) ->setETag($response->getHeader(Header::ETAG)) ->setPath($responseUri->getPath()); - + return $object; } - } diff --git a/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Upload/TransferState.php b/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Upload/TransferState.php index 49a3ff277..ce2a4f475 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Upload/TransferState.php +++ b/rackspace/php-opencloud/lib/OpenCloud/ObjectStore/Upload/TransferState.php @@ -1,11 +1,18 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\ObjectStore\Upload; @@ -33,6 +40,7 @@ class TransferState public static function factory() { $self = new self(); + return $self->init(); } @@ -66,7 +74,7 @@ public function isRunning() public function init() { $this->running = true; - + return $this; } @@ -76,8 +84,7 @@ public function init() public function cancel() { $this->running = false; - + return $this; } - -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/OpenStack.php b/rackspace/php-opencloud/lib/OpenCloud/OpenStack.php index 5c6a28a92..e970b9912 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/OpenStack.php +++ b/rackspace/php-opencloud/lib/OpenCloud/OpenStack.php @@ -1,29 +1,36 @@ - * @author Glen Campbell + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud; -use OpenCloud\Common\Constants\Header; -use OpenCloud\Common\Constants\Mime; +use Guzzle\Http\Url; +use OpenCloud\Common\Exceptions; use OpenCloud\Common\Http\Client; +use OpenCloud\Common\Http\Message\Formatter; use OpenCloud\Common\Http\Message\RequestSubscriber; use OpenCloud\Common\Lang; -use OpenCloud\Common\Exceptions; -use OpenCloud\Common\Service\ServiceBuilder; +use OpenCloud\Common\Log\Logger; use OpenCloud\Common\Service\Catalog; -use OpenCloud\Common\Http\Message\Formatter; -use OpenCloud\Identity\Service as IdentityService; -use OpenCloud\Identity\Resource\Token; +use OpenCloud\Common\Service\ServiceBuilder; use OpenCloud\Identity\Resource\Tenant; +use OpenCloud\Identity\Resource\Token; use OpenCloud\Identity\Resource\User; -use Guzzle\Http\Url; +use OpenCloud\Identity\Service as IdentityService; +use Psr\Log\LoggerInterface; define('RACKSPACE_US', 'https://identity.api.rackspacecloud.com/v2.0/'); define('RACKSPACE_UK', 'https://lon.identity.api.rackspacecloud.com/v2.0/'); @@ -56,7 +63,7 @@ class OpenStack extends Client private $catalog; /** - * @var \OpenCloud\Common\Log\LoggerInterface The object responsible for logging output + * @var LoggerInterface The object responsible for logging output */ private $logger; @@ -72,17 +79,19 @@ class OpenStack extends Client public function __construct($url, array $secret, array $options = array()) { - $this->getLogger()->info(Lang::translate('Initializing OpenStack client')); + if (isset($options['logger']) && $options['logger'] instanceof LoggerInterface) { + $this->setLogger($options['logger']); + } $this->setSecret($secret); $this->setAuthUrl($url); parent::__construct($url, $options); - + $this->addSubscriber(RequestSubscriber::getInstance()); $this->setDefaultOption('headers/Accept', 'application/json'); } - + /** * Set the credentials for the client * @@ -92,25 +101,25 @@ public function __construct($url, array $secret, array $options = array()) public function setSecret(array $secret = array()) { $this->secret = $secret; - + return $this; } - + /** * Get the secret. - * + * * @return array */ public function getSecret() { return $this->secret; } - + /** * Set the token. If a string is passed in, the SDK assumes you want to set the ID of the full Token object * and sets this property accordingly. For any other data type, it assumes you want to populate the Token object. * This ambiguity arises due to backwards compatibility. - * + * * @param string $token * @return $this */ @@ -119,12 +128,10 @@ public function setToken($token) $identity = IdentityService::factory($this); if (is_string($token)) { - if (!$this->token) { $this->setTokenObject($identity->resource('Token')); } $this->token->setId($token); - } else { $this->setTokenObject($identity->resource('Token', $token)); } @@ -143,7 +150,7 @@ public function getToken() } /** - * Set the full toke object + * Set the full token object */ public function setTokenObject(Token $token) { @@ -157,59 +164,58 @@ public function getTokenObject() { return $this->token; } - + /** * @deprecated */ public function setExpiration($expiration) { - $this->getLogger()->deprecated(__METHOD__, '::getTokenObject()->setExpires()'); + $this->getLogger()->warning(Logger::deprecated(__METHOD__, '::getTokenObject()->setExpires()')); if ($this->getTokenObject()) { $this->getTokenObject()->setExpires($expiration); } + return $this; } - + /** * @deprecated */ public function getExpiration() { - $this->getLogger()->deprecated(__METHOD__, '::getTokenObject()->getExpires()'); + $this->getLogger()->warning(Logger::deprecated(__METHOD__, '::getTokenObject()->getExpires()')); if ($this->getTokenObject()) { return $this->getTokenObject()->getExpires(); } } - + /** - * Set the tenant. If an integer is passed in, the SDK assumes you want to set the ID of the full Tenant object - * and sets this property accordingly. For any other data type, it assumes you want to populate the Tenant object. - * This ambiguity arises due to backwards compatibility. - * - * @param string $tenant + * Set the tenant. If an integer or string is passed in, the SDK assumes you want to set the ID of the full + * Tenant object and sets this property accordingly. For any other data type, it assumes you want to populate + * the Tenant object. This ambiguity arises due to backwards compatibility. + * + * @param mixed $tenant * @return $this */ public function setTenant($tenant) { $identity = IdentityService::factory($this); - if (is_numeric($tenant)) { - + if (is_numeric($tenant) || is_string($tenant)) { if (!$this->tenant) { $this->setTenantObject($identity->resource('Tenant')); } $this->tenant->setId($tenant); - } else { $this->setTenantObject($identity->resource('Tenant', $tenant)); } - + return $this; } - + /** * Returns the tenant ID only (backwards compatibility). - * + * * @return string */ public function getTenant() @@ -236,10 +242,10 @@ public function getTenantObject() { return $this->tenant; } - + /** * Set the service catalog. - * + * * @param mixed $catalog * @return $this */ @@ -249,10 +255,10 @@ public function setCatalog($catalog) return $this; } - + /** * Get the service catalog. - * + * * @return array */ public function getCatalog() @@ -261,32 +267,44 @@ public function getCatalog() } /** - * @param Common\Log\LoggerInterface $logger + * @param LoggerInterface $logger + * * @return $this */ - public function setLogger(Common\Log\LoggerInterface $logger) + public function setLogger(LoggerInterface $logger) { $this->logger = $logger; + return $this; } /** - * @return Common\Log\LoggerInterface + * @return LoggerInterface */ public function getLogger() { if (null === $this->logger) { $this->setLogger(new Common\Log\Logger); } + return $this->logger; } - + + /** + * @return bool + */ + public function hasLogger() + { + return (null !== $this->logger); + } + /** * @deprecated */ public function hasExpired() { - $this->getLogger()->deprecated(__METHOD__, 'getTokenObject()->hasExpired()'); + $this->getLogger()->warning(Logger::deprecated(__METHOD__, 'getTokenObject()->hasExpired()')); + return $this->getTokenObject() && $this->getTokenObject()->hasExpired(); } @@ -299,7 +317,6 @@ public function hasExpired() public function getCredentials() { if (!empty($this->secret['username']) && !empty($this->secret['password'])) { - $credentials = array('auth' => array( 'passwordCredentials' => array( 'username' => $this->secret['username'], @@ -314,10 +331,9 @@ public function getCredentials() } return json_encode($credentials); - } else { throw new Exceptions\CredentialError( - Lang::translate('Unrecognized credential secret') + Lang::translate('Unrecognized credential secret') ); } } @@ -328,8 +344,9 @@ public function getCredentials() */ public function setAuthUrl($url) { - $this->authUrl = Url::factory($url); - return $this; + $this->authUrl = Url::factory($url); + + return $this; } /** @@ -337,7 +354,7 @@ public function setAuthUrl($url) */ public function getAuthUrl() { - return $this->authUrl; + return $this->authUrl; } /** @@ -366,6 +383,10 @@ public function getUser() */ public function authenticate() { + // OpenStack APIs will return a 401 if an expired X-Auth-Token is sent, + // so we need to reset the value before authenticating for another one. + $this->updateTokenHeader(''); + $identity = IdentityService::factory($this); $response = $identity->generateToken($this->getCredentials()); @@ -380,7 +401,7 @@ public function authenticate() } // Set X-Auth-Token HTTP request header - $this->updateTokenHeader(); + $this->updateTokenHeader($this->getToken()); } /** @@ -400,6 +421,7 @@ public function exportCredentials() if ($this->hasExpired()) { $this->authenticate(); } + return array( 'token' => $this->getToken(), 'expiration' => $this->getExpiration(), @@ -417,7 +439,7 @@ public function importCredentials(array $values) { if (!empty($values['token'])) { $this->setToken($values['token']); - $this->updateTokenHeader(); + $this->updateTokenHeader($this->getToken()); } if (!empty($values['expiration'])) { $this->setExpiration($values['expiration']); @@ -433,12 +455,11 @@ public function importCredentials(array $values) /** * Sets the X-Auth-Token header. If no value is explicitly passed in, the current token is used. * - * @param string $token Optional value of token. + * @param string $token Value of header. * @return void */ - private function updateTokenHeader($token = null) + private function updateTokenHeader($token) { - $token = $token ?: $this->getToken(); $this->setDefaultOption('headers/X-Auth-Token', (string) $token); } @@ -453,8 +474,8 @@ private function updateTokenHeader($token = null) public function objectStoreService($name = null, $region = null, $urltype = null) { return ServiceBuilder::factory($this, 'OpenCloud\ObjectStore\Service', array( - 'name' => $name, - 'region' => $region, + 'name' => $name, + 'region' => $region, 'urlType' => $urltype )); } @@ -470,8 +491,8 @@ public function objectStoreService($name = null, $region = null, $urltype = null public function computeService($name = null, $region = null, $urltype = null) { return ServiceBuilder::factory($this, 'OpenCloud\Compute\Service', array( - 'name' => $name, - 'region' => $region, + 'name' => $name, + 'region' => $region, 'urlType' => $urltype )); } @@ -488,8 +509,8 @@ public function computeService($name = null, $region = null, $urltype = null) public function orchestrationService($name = null, $region = null, $urltype = null) { return ServiceBuilder::factory($this, 'OpenCloud\Orchestration\Service', array( - 'name' => $name, - 'region' => $region, + 'name' => $name, + 'region' => $region, 'urlType' => $urltype )); } @@ -505,8 +526,8 @@ public function orchestrationService($name = null, $region = null, $urltype = nu public function volumeService($name = null, $region = null, $urltype = null) { return ServiceBuilder::factory($this, 'OpenCloud\Volume\Service', array( - 'name' => $name, - 'region' => $region, + 'name' => $name, + 'region' => $region, 'urlType' => $urltype )); } @@ -520,6 +541,60 @@ public function identityService() { $service = IdentityService::factory($this); $this->authenticate(); + return $service; } -} \ No newline at end of file + + /** + * Creates a new Glance service + * + * @param string $name The name of the service as it appears in the Catalog + * @param string $region The region (DFW, IAD, ORD, LON, SYD) + * @param string $urltype The URL type ("publicURL" or "internalURL") + * @return Common\Service\ServiceInterface + */ + public function imageService($name = null, $region = null, $urltype = null) + { + return ServiceBuilder::factory($this, 'OpenCloud\Image\Service', array( + 'name' => $name, + 'region' => $region, + 'urlType' => $urltype + )); + } + + /** + * Creates a new Networking (Neutron) service object + * + * @param string $name The name of the service as it appears in the Catalog + * @param string $region The region (DFW, IAD, ORD, LON, SYD) + * @param string $urltype The URL type ("publicURL" or "internalURL") + * @return \OpenCloud\Networking\Service + * @codeCoverageIgnore + */ + public function networkingService($name = null, $region = null, $urltype = null) + { + return ServiceBuilder::factory($this, 'OpenCloud\Networking\Service', array( + 'name' => $name, + 'region' => $region, + 'urlType' => $urltype + )); + } + + /** + * Creates a new CDN (Poppy) service object + * + * @param string $name The name of the service as it appears in the Catalog + * @param string $region The region (DFW, IAD, ORD, LON, SYD) + * @param string $urltype The URL type ("publicURL" or "internalURL") + * @return \OpenCloud\Cdn\Service + * @codeCoverageIgnore + */ + public function cdnService($name = null, $region = null, $urltype = null) + { + return ServiceBuilder::factory($this, 'OpenCloud\CDN\Service', array( + 'name' => $name, + 'region' => $region, + 'urlType' => $urltype + )); + } +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Orchestration/Resource.php b/rackspace/php-opencloud/lib/OpenCloud/Orchestration/Resource.php deleted file mode 100644 index ff10b4dd3..000000000 --- a/rackspace/php-opencloud/lib/OpenCloud/Orchestration/Resource.php +++ /dev/null @@ -1,79 +0,0 @@ - - */ - -namespace OpenCloud\Orchestration; - -use OpenCloud\Common\PersistentObject; - -/** - * @codeCoverageIgnore - */ -class Resource extends PersistentObject -{ - protected $links; - protected $logical_resource_id; - protected $physical_resource_id; - protected $resource_status; - protected $resource_status_reason; - protected $resource_type; - protected $updated_time; - - protected static $url_resource = 'resources'; - protected static $json_name = 'resource'; - - public function create($info = null) - { - $this->noCreate(); - } - - public function id() - { - return $this->physical_resource_id; - } - - protected function primaryKeyField() - { - return 'physical_resource_id'; - } - - public function name() - { - return $this->logical_resource_id; - } - - public function type() - { - return $this->resource_type; - } - - public function status() - { - return $this->resource_status; - } - - public function get() - { - $service = $this->getParent()->getService(); - - switch ($this->resource_type) { - case 'AWS::EC2::Instance': - $objSvc = 'Compute'; - $method = 'Server'; - $name = 'nova'; - break; - default: - throw new Exception(sprintf( - 'Unknown resource type: %s', - $this->resource_type - )); - } - - return $service->connection()->$objSvc($name, $service->region())->$method($this->id()); - } -} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Orchestration/Resource/BuildInfo.php b/rackspace/php-opencloud/lib/OpenCloud/Orchestration/Resource/BuildInfo.php new file mode 100644 index 000000000..04152794c --- /dev/null +++ b/rackspace/php-opencloud/lib/OpenCloud/Orchestration/Resource/BuildInfo.php @@ -0,0 +1,40 @@ +refreshFromParent(); + } +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Orchestration/Resource/Event.php b/rackspace/php-opencloud/lib/OpenCloud/Orchestration/Resource/Event.php new file mode 100644 index 000000000..bf0da51a8 --- /dev/null +++ b/rackspace/php-opencloud/lib/OpenCloud/Orchestration/Resource/Event.php @@ -0,0 +1,54 @@ + 'time', + 'resource_name' => 'resourceName', + 'logical_resource_id' => 'resourceLogicalId', + 'physical_resource_id' => 'resourcePhysicalId', + 'resource_status' => 'resourceStatus', + 'resource_status_reason' => 'resourceStatusReason', + 'resource_type' => 'resourceType', + 'resource_properties' => 'resourceProperties' + ); +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Orchestration/Resource/Resource.php b/rackspace/php-opencloud/lib/OpenCloud/Orchestration/Resource/Resource.php new file mode 100644 index 000000000..431251e73 --- /dev/null +++ b/rackspace/php-opencloud/lib/OpenCloud/Orchestration/Resource/Resource.php @@ -0,0 +1,95 @@ + 'name', + 'resource_status' => 'status', + 'resource_status_reason' => 'statusReason', + 'logical_resource_id' => 'logicalId', + 'physical_resource_id' => 'physicalId', + 'required_by' => 'requiredBy', + 'updated_time' => 'updatedTime', + 'resource_type' => 'type' + ); + + public function primaryKeyField() + { + return 'name'; + } + + /** + * Returns metadata properties associated with this Resource + * + * @return \stdClass + */ + public function getMetadata() + { + $url = clone $this->getUrl(); + $url->addPath('metadata'); + + $response = $this->getClient()->get($url)->send(); + $json = Formatter::decode($response); + + return $json->metadata; + } + + /** + * Returns a list of Events associated with this Resource + * + * @param array $params + * @return \OpenCloud\Common\Collection\PaginatedIterator + */ + public function listEvents(array $params = array()) + { + $url = clone $this->getUrl(); + $url->addPath(Event::resourceName())->setQuery($params); + + return $this->getService()->resourceList('Event', $url, $this); + } + + public function getEvent($id) + { + return $this->getService()->resource('Event', $id, $this); + } +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Orchestration/Resource/ResourceType.php b/rackspace/php-opencloud/lib/OpenCloud/Orchestration/Resource/ResourceType.php new file mode 100644 index 000000000..4f6a81bf9 --- /dev/null +++ b/rackspace/php-opencloud/lib/OpenCloud/Orchestration/Resource/ResourceType.php @@ -0,0 +1,61 @@ + 'resourceType', + 'properties' => 'resourceTypeProperties' + ); + + /** + * Returns the template representation for this resource type. + * + * @return String template representation + */ + public function getTemplate() + { + $url = clone $this->getUrl(); + $url->addPath('template'); + + $response = $this->getClient()->get($url)->send(); + return $response->getBody(true); + } + + protected function primaryKeyField() + { + return 'resourceType'; + } +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Orchestration/Resource/Stack.php b/rackspace/php-opencloud/lib/OpenCloud/Orchestration/Resource/Stack.php new file mode 100644 index 000000000..53162b1ec --- /dev/null +++ b/rackspace/php-opencloud/lib/OpenCloud/Orchestration/Resource/Stack.php @@ -0,0 +1,234 @@ + 'parentStack', + 'disable_rollback' => 'disableRollback', + 'stack_name' => 'name', + 'stack_status' => 'status', + 'stack_status_reason' => 'statusReason', + 'creation_time' => 'creationTime', + 'updated_time' => 'updatedTime', + 'timeout_mins' => 'timeoutMins', + 'template_url' => 'templateUrl', + 'adopt_stack_data' => 'adoptStackData' + ); + + protected $createKeys = array( + 'name', + 'templateUrl', + 'template', + 'environment', + 'files', + 'parameters', + 'timeoutMins', + 'adoptStackData' + ); + + protected $updateKeys = array( + 'templateUrl', + 'template', + 'environment', + 'files', + 'parameters', + 'timeoutMins' + ); + + protected function createJson() + { + $createJson = parent::createJson(); + return $createJson->{self::$json_name}; + } + + protected function updateJson($params = array()) + { + $updateJson = parent::updateJson($params); + return $updateJson->{self::$json_name}; + } + + /** + * Creates a new stack by adopting resources from an abandoned stack + * + * @param array $params Adopt stack parameters + * @return Guzzle\Http\Message\Response + */ + public function adopt($params) + { + // Validate that required parameters are provided + $requiredParameterName = 'adoptStackData'; + if (!array_key_exists($requiredParameterName, $params)) { + throw new \InvalidArgumentException($requiredParameterName . ' is a required option'); + } + + return $this->create($params); + } + + /** + * Previews the stack without actually creating it + * + * @param array $params Preview stack parameters + * @return Guzzle\Http\Message\Response + */ + public function preview($params = array()) + { + // set parameters + if (!empty($params)) { + $this->populate($params, false); + } + + // construct the JSON + $json = json_encode($this->createJson()); + $this->checkJsonError(); + + $previewUrl = $this->previewUrl(); + $response = $this->getClient()->post($previewUrl, self::getJsonHeader(), $json)->send(); + + $decoded = $this->parseResponse($response); + $this->populate($decoded); + + return $response; + } + + /** + * Abandons the stack and returns abandoned stack data. + * + * @return string Abandoned stack data (which could be passed to the adopt stack operation as adoptStackData). + */ + public function abandon() + { + $abandonUrl = $this->abandonUrl(); + $response = $this->getClient()->delete($abandonUrl)->send(); + return $response->getBody(true); + } + + /** + * Returns a Resource object associated with this Stack + * + * @param string $name Stack resource name + * @return Resource object + */ + public function getResource($name) + { + return $this->getService()->resource('Resource', $name, $this); + } + + /** + * Returns a list of Resources associated with this Stack + * + * @param array $params + * @return \OpenCloud\Common\Collection\PaginatedIterator + */ + public function listResources(array $params = array()) + { + $url = clone $this->getUrl(); + $url->addPath(Resource::resourceName())->setQuery($params); + + return $this->getService()->resourceList('Resource', $url, $this); + } + + /** + * Returns a list of Events associated with this Stack + * + * @param array $params + * @return \OpenCloud\Common\Collection\PaginatedIterator + */ + public function listEvents(array $params = array()) + { + $url = clone $this->getUrl(); + $url->addPath(Event::resourceName())->setQuery($params); + + return $this->getService()->resourceList('Event', $url, $this); + } + + /** + * Iterator use only + */ + public function event($id) + { + return $this->getService()->resource('Event', $id, $this); + } + + /** + * Returns the template for this stack. + * + * @return String template + */ + public function getStackTemplate() + { + $url = clone $this->getUrl(); + $url->addPath('template'); + + $response = $this->getClient()->get($url)->send(); + return $response->getBody(true); + } + + protected function previewUrl() + { + $url = clone $this->getParent()->getUrl(); + $url->addPath(self::resourceName()); + $url->addPath('preview'); + + return $url; + } + + protected function abandonUrl() + { + $url = clone $this->getUrl(); + $url->addPath('abandon'); + + return $url; + } + + protected function primaryKeyField() + { + return 'name'; + } +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Orchestration/Service.php b/rackspace/php-opencloud/lib/OpenCloud/Orchestration/Service.php index b01a59fd7..81c1a3e53 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Orchestration/Service.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Orchestration/Service.php @@ -1,23 +1,36 @@ + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Orchestration; +use Guzzle\Http\Exception\ClientErrorResponseException; +use OpenCloud\Common\Exceptions\InvalidTemplateError; +use OpenCloud\Common\Http\Message\Formatter; use OpenCloud\Common\Service\CatalogService; +use OpenCloud\Orchestration\Resource\ResourceType; +use OpenCloud\Orchestration\Resource\Stack; /** * The Orchestration class represents the OpenStack Heat service. * - * Heat is a service to orchestrate multiple composite cloud applications using - * the AWS CloudFormation template format, through both an OpenStack-native ReST + * Heat is a service to orchestrate multiple composite cloud applications using + * the AWS CloudFormation template format, through both an OpenStack-native ReST * API and a CloudFormation-compatible Query API. - * + * * @codeCoverageIgnore */ class Service extends CatalogService @@ -28,21 +41,146 @@ class Service extends CatalogService /** * Returns a Stack object associated with this Orchestration service * - * @api - * @param string $id - the stack with the ID is retrieved - * @returns Stack object + * @param string $name Name of stack to retrieve + * @return Stack object + */ + public function stack($name = null) + { + return $this->resource('Stack', $name); + } + + /** + * Previews a Stack from a template and returns it. + * + * @param array $params Stack preview parameters + * @return Stack Object representing previewed stack + */ + public function previewStack($params = array()) + { + $stack = $this->stack(); + $stack->preview($params); + return $stack; + } + + /** + * Creates a new Stack and returns it. + * + * @param array $params Stack creation parameters + * @return Stack Object representing created stack + */ + public function createStack($params = array()) + { + $stack = $this->stack(); + $stack->create($params); + return $stack; + } + + /** + * Adopts a Stack and returns it. + * + * @param array $params Stack adoption parameters + * @return Stack Object representing adopted stack + */ + public function adoptStack($params = array()) + { + $stack = $this->stack(); + $stack->adopt($params); + return $stack; + } + + /** + * Returns a Stack object associated with this Orchestration service + * + * @param string $name Name of stack to retrieve + * @return Stack object + */ + public function getStack($name) + { + return $this->stack($name); + } + + /** + * Returns a list of stacks you created + * + * @param array $params + * @return \OpenCloud\Common\Collection\PaginatedIterator */ - public function stack($id = null) + public function listStacks(array $params = array()) { - return new Stack($this, $id); + $url = clone $this->getUrl(); + $url->addPath(Stack::resourceName())->setQuery($params); + + return $this->resourceList('Stack', $url); } - + + /** + * Returns a list of resource types available + * + * @param array $params + * @return \OpenCloud\Common\Collection\PaginatedIterator + */ + public function listResourceTypes(array $params = array()) + { + $url = clone $this->getUrl(); + $url->addPath(ResourceType::resourceName())->setQuery($params); + + return $this->resourceList('ResourceType', $url); + } + + /** + * Returns a ResourceType object associated with this Orchestration service + * + * @param string $id - the resource type with the ID is retrieved + * @return ResourceType object + */ + public function getResourceType($id) + { + return $this->resource('ResourceType', $id); + } + + /** + * Returns a BuildInfo object associated with this Orchestration service + * + * @return BuildInfo object + */ + public function getBuildInfo() + { + $buildInfo = $this->resource('BuildInfo'); + $buildInfo->refresh(); + return $buildInfo; + } + + /** + * Validates the given template + * + * @throws InvalidTemplateError if template is invalid + */ + public function validateTemplate(array $params = array()) + { + $url = clone $this->getUrl(); + $url->addPath('validate'); + + // Aliases + if (array_key_exists('templateUrl', $params)) { + $params['template_url'] = $params['templateUrl']; + } + + $json = json_encode($params); + + try { + $this->getClient()->post($url, self::getJsonHeader(), $json)->send(); + } catch (ClientErrorResponseException $e) { + $response = Formatter::decode($e->getResponse()); + throw new InvalidTemplateError($response->explanation, $response->code); + } + } + /** * Return namespaces. - * + * * @return array */ - public function namespaces() + public function namespaces() { return array(); } diff --git a/rackspace/php-opencloud/lib/OpenCloud/Orchestration/Stack.php b/rackspace/php-opencloud/lib/OpenCloud/Orchestration/Stack.php deleted file mode 100644 index 84b18d9d5..000000000 --- a/rackspace/php-opencloud/lib/OpenCloud/Orchestration/Stack.php +++ /dev/null @@ -1,196 +0,0 @@ - - */ - -namespace OpenCloud\Orchestration; - -use OpenCloud\Common\PersistentObject; -use OpenCloud\Exceptions\CreateError; - -/** - * The Stack class requires a CloudFormation template and may contain additional - * parameters for that template. - * - * A Stack is always associated with an (Orchestration) Service. - * - * @codeCoverageIgnore - */ -class Stack extends PersistentObject -{ - /** - * Identifier of stack. - * - * @var string - */ - protected $id; - - /** - * The name associated with the stack. Must be unique within your account, - * contain only alphanumeric characters (case sensitive) and start with an - * alpha character. Maximum length of the name is 255 characters. - * - * @var string - */ - protected $stack_name; - - /** - * A list of Parameter structures that specify input parameters for the stack. - * - * @var mixed - */ - protected $parameters; - - /** - * Structure containing the template body. - * - * @var string - */ - protected $template; - - /** - * Set to true to disable rollback of the stack if stack creation failed. - * - * @var bool - */ - protected $disable_rollback; - - /** - * Description of stack. - * - * @var string - */ - protected $description; - - /** - * @var type - */ - protected $stack_status_reason; - - /** - * @var type - */ - protected $outputs; - - /** - * @var type - */ - protected $creation_time; - - /** - * Array of stack lists. - * - * @var array - */ - protected $links; - - /** - * The list of capabilities that you want to allow in the stack. - * - * @var mixed - */ - protected $capabilities; - - /** - * The Simple Notification Service topic ARNs to publish stack related events. - * - * @var mixed - */ - protected $notification_topics; - - /** - * The amount of time that can pass before the stack status becomes - * CREATE_FAILED; if DisableRollback is not set or is set to false, the - * stack will be rolled back. - * - * @var string - */ - protected $timeout_mins; - - /** - * @var type - */ - protected $stack_status; - - /** - * @var type - */ - protected $updated_time; - - /** - * @var type - */ - protected $template_description; - - protected static $json_name = "stack"; - protected static $url_resource = "stacks"; - protected $createKeys = array( - 'template', - 'stack_name' - ); - - /** - * {@inheritDoc} - */ - protected function createJson() - { - $pk = $this->primaryKeyField(); - - if (!empty($this->{$pk})) { - throw new CreateError(sprintf( - 'Stack is already created and has ID of %s', - $this->$pk - )); - } - - $object = (object) array('disable_rollback' => false, 'timeout_mins' => 60); - - foreach ($this->createKeys as $property) { - if (empty($this->$property)) { - throw new CreateError(sprintf( - 'Cannot create Stack with null %s', - $property - )); - } else { - $object->$property = $this->$property; - } - } - - if (null !== $this->parameters) { - $object->parameters = $this->parameters; - } - - return $object; - } - - public function name() - { - return $this->stack_name; - } - - public function status() - { - return $this->stack_status; - } - - public function resource($id = null) - { - $resource = new Resource($this->getService()); - $resource->setParent($this); - $resource->populate($id); - return $resource; - } - - public function resources() - { - return $this->getService()->collection( - 'OpenCloud\Orchestration\Resource', - $this->url('resources'), - $this - ); - } -} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Queues/Collection/MessageIterator.php b/rackspace/php-opencloud/lib/OpenCloud/Queues/Collection/MessageIterator.php new file mode 100644 index 000000000..0f4140936 --- /dev/null +++ b/rackspace/php-opencloud/lib/OpenCloud/Queues/Collection/MessageIterator.php @@ -0,0 +1,29 @@ +nextUrl || + ($this->position % $this->getOption('limit.page') == 0); + } +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Queues/Exception/DeleteMessageException.php b/rackspace/php-opencloud/lib/OpenCloud/Queues/Exception/DeleteMessageException.php index 48b1b02c0..50a1ee304 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Queues/Exception/DeleteMessageException.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Queues/Exception/DeleteMessageException.php @@ -1,13 +1,22 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Queues\Exception; -class DeleteMessageException extends \Exception {} \ No newline at end of file +class DeleteMessageException extends \Exception +{ +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Queues/Exception/MessageException.php b/rackspace/php-opencloud/lib/OpenCloud/Queues/Exception/MessageException.php index 0cb3c64ba..20ae82e65 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Queues/Exception/MessageException.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Queues/Exception/MessageException.php @@ -1,15 +1,22 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Queues\Exception; class MessageException extends \Exception { -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Queues/Exception/QueueException.php b/rackspace/php-opencloud/lib/OpenCloud/Queues/Exception/QueueException.php index e7c2bcd59..6b759fe9d 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Queues/Exception/QueueException.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Queues/Exception/QueueException.php @@ -1,13 +1,22 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Queues\Exception; -class QueueException extends \Exception {} \ No newline at end of file +class QueueException extends \Exception +{ +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Queues/Exception/QueueMetadataException.php b/rackspace/php-opencloud/lib/OpenCloud/Queues/Exception/QueueMetadataException.php index 3539a4f61..05a490e5c 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Queues/Exception/QueueMetadataException.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Queues/Exception/QueueMetadataException.php @@ -1,13 +1,22 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Queues\Exception; -class QueueMetadataException extends \Exception {} \ No newline at end of file +class QueueMetadataException extends \Exception +{ +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Queues/Resource/Claim.php b/rackspace/php-opencloud/lib/OpenCloud/Queues/Resource/Claim.php index eb4adf915..5d4e2dc21 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Queues/Resource/Claim.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Queues/Resource/Claim.php @@ -1,10 +1,18 @@ + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Queues\Resource; @@ -12,60 +20,59 @@ use OpenCloud\Common\PersistentObject; /** - * A worker claims or checks out a message to perform a task. Doing so prevents + * A worker claims or checks out a message to perform a task. Doing so prevents * other workers from attempting to process the same messages. */ class Claim extends PersistentObject { - const LIMIT_DEFAULT = 10; const GRACE_DEFAULT = 43200; const TTL_DEFAULT = 43200; - + /** - * @var string + * @var string */ private $id; - + /** - * @var int + * @var int */ private $age; - + /** * @var array An array of messages. */ private $messages; - + /** - * How long the server should wait before releasing the claim in seconds. - * The ttl value must be between 60 and 43200 seconds (12 hours is the + * How long the server should wait before releasing the claim in seconds. + * The ttl value must be between 60 and 43200 seconds (12 hours is the * default but is configurable). - * - * @var int + * + * @var int */ private $ttl; - + /** - * The message grace period in seconds. The value of grace must be between - * 60 and 43200 seconds (12 hours the default, but configurable). The server - * extends the lifetime of claimed messages to be at least as long as the - * lifetime of the claim itself, plus a specified grace period to deal with - * crashed workers (up to 1209600 or 14 days including claim lifetime). If a - * claimed message would normally live longer than the grace period, it's + * The message grace period in seconds. The value of grace must be between + * 60 and 43200 seconds (12 hours the default, but configurable). The server + * extends the lifetime of claimed messages to be at least as long as the + * lifetime of the claim itself, plus a specified grace period to deal with + * crashed workers (up to 1209600 or 14 days including claim lifetime). If a + * claimed message would normally live longer than the grace period, it's * expiration will not be adjusted. - * - * @var int + * + * @var int */ private $grace; - + /** * Link. - * - * @var string + * + * @var string */ private $href; - + protected static $url_resource = 'claims'; protected static $json_name = ''; @@ -80,6 +87,7 @@ public function setHref($href) $paths = explode('/', $href); $this->id = end($paths); $this->href = $href; + return $this; } @@ -103,20 +111,23 @@ public function create($params = array()) { return $this->noCreate(); } - + /** - * Updates the current Claim. It is recommended that you periodically renew - * claims during long-running batches of work to avoid loosing a claim in - * the middle of processing a message. This is done by setting a new TTL for - * the claim (which may be different from the original TTL). The server will + * Updates the current Claim. It is recommended that you periodically renew + * claims during long-running batches of work to avoid losing a claim in + * the middle of processing a message. This is done by setting a new TTL for + * the claim (which may be different from the original TTL). The server will * then reset the age of the claim and apply the new TTL. * {@inheritDoc} */ public function update($params = array()) { + $grace = (isset($params['grace'])) ? $params['grace'] : $this->getGrace(); + $ttl = (isset($params['ttl'])) ? $params['ttl'] : $this->getGrace(); + $object = (object) array( - 'grace' => $this->getGrace(), - 'ttl' => $this->getTtl() + 'grace' => (int) $grace, + 'ttl' => (int) $ttl ); $json = json_encode($object); @@ -124,5 +135,4 @@ public function update($params = array()) return $this->getClient()->patch($this->url(), self::getJsonHeader(), $json)->send(); } - -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Queues/Resource/Message.php b/rackspace/php-opencloud/lib/OpenCloud/Queues/Resource/Message.php index 0fa486dab..f1164717f 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Queues/Resource/Message.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Queues/Resource/Message.php @@ -1,10 +1,18 @@ + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Queues\Resource; @@ -14,68 +22,68 @@ use OpenCloud\Queues\Exception\DeleteMessageException; /** - * A message is a task, a notification, or any meaningful data that gets posted - * to the queue. A message exists until it is deleted by a recipient or - * automatically by the system based on a TTL (time-to-live) value. + * A message is a task, a notification, or any meaningful data that gets posted + * to the queue. A message exists until it is deleted by a recipient or + * automatically by the system based on a TTL (time-to-live) value. */ class Message extends PersistentObject { - /** - * @var string + * @var string */ private $id; - + /** * The number of seconds since ts, relative to the server's clock. - * - * @var int + * + * @var int */ private $age; - + /** - * Defines how long a message will be accessible. The message expires after + * Defines how long a message will be accessible. The message expires after * ($ttl - $age) seconds. - * - * @var int + * + * @var int */ private $ttl = 600; - + /** - * The arbitrary document submitted along with the original request to post + * The arbitrary document submitted along with the original request to post * the message. - * - * @var mixed + * + * @var mixed */ private $body; - + /** - * An opaque relative URI that the client can use to uniquely identify a + * An opaque relative URI that the client can use to uniquely identify a * message resource, and interact with it. - * - * @var string + * + * @var string */ private $href; - + protected static $url_resource = 'messages'; protected static $json_collection_name = 'messages'; protected static $json_name = ''; - + /** * Set href (and ID). - * + * * @param string $href * @return self */ public function setHref($href) { // We have to extract the ID out of the Href. Nice... - preg_match('#.+/([\w\d]+)\?claim_id\=.+$#', $href, $match); + preg_match('#.+/([\w]+)#', $href, $match); if (!empty($match)) { $this->setId($match[1]); } $this->href = $href; + return $this; } @@ -98,6 +106,7 @@ public function createJson() public function create($params = array()) { $this->getLogger()->alert('Please use Queue::createMessage() or Queue::createMessages()'); + return $this->noCreate(); } @@ -105,22 +114,22 @@ public function update($params = array()) { return $this->noUpdate(); } - + /** * This operation immediately deletes the specified message. - * - * @param string $claimId Specifies that the message should be deleted - * only if it has the specified claim ID, and that claim has not expired. - * This is useful for ensuring only one agent processes any given - * message. Whenever a worker client's claim expires before it has a - * chance to delete a message it has processed, the worker must roll - * back any actions it took based on that message because another worker - * will now be able to claim and process the same message. - * - * If you do *not* specify $claimId, but the message is claimed, the - * operation fails. You can only delete claimed messages by providing + * + * @param string $claimId Specifies that the message should be deleted + * only if it has the specified claim ID, and that claim has not expired. + * This is useful for ensuring only one agent processes any given + * message. Whenever a worker client's claim expires before it has a + * chance to delete a message it has processed, the worker must roll + * back any actions it took based on that message because another worker + * will now be able to claim and process the same message. + * + * If you do *not* specify $claimId, but the message is claimed, the + * operation fails. You can only delete claimed messages by providing * an appropriate $claimId. - * + * * @return bool * @throws DeleteMessageException */ @@ -130,11 +139,10 @@ public function delete($claimId = null) $this->getClient() ->delete($url) ->send(); - + return true; } - /** * If this message has been claimed, retrieve the claim id. * @@ -143,7 +151,7 @@ public function delete($claimId = null) public function getClaimIdFromHref() { $url = Url::factory($this->href); + return $url->getQuery()->get('claim_id'); } - } diff --git a/rackspace/php-opencloud/lib/OpenCloud/Queues/Resource/Queue.php b/rackspace/php-opencloud/lib/OpenCloud/Queues/Resource/Queue.php index d4ac3befc..b6817e0d6 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Queues/Resource/Queue.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Queues/Resource/Queue.php @@ -1,61 +1,69 @@ + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Queues\Resource; use Guzzle\Http\Url; -use OpenCloud\Common\PersistentObject; -use OpenCloud\Common\Exceptions\InvalidArgumentError; -use OpenCloud\Queues\Exception; -use OpenCloud\Common\Metadata; use OpenCloud\Common\Collection\PaginatedIterator; use OpenCloud\Common\Http\Message\Formatter; +use OpenCloud\Common\Resource\PersistentResource; +use OpenCloud\Queues\Exception; +use OpenCloud\Queues\Collection\MessageIterator; +use OpenCloud\Common\Metadata; /** - * A queue holds messages. Ideally, a queue is created per work type. For example, - * if you want to compress files, you would create a queue dedicated to this job. + * A queue holds messages. Ideally, a queue is created per work type. For example, + * if you want to compress files, you would create a queue dedicated to this job. * Any application that reads from this queue would only compress files. */ -class Queue extends PersistentObject +class Queue extends PersistentResource { /** * Maximum number of messages that can be posted at once. */ const MAX_POST_MESSAGES = 10; - + /** - * The name given to the queue. The name MUST NOT exceed 64 bytes in length, + * The name given to the queue. The name MUST NOT exceed 64 bytes in length, * and is limited to US-ASCII letters, digits, underscores, and hyphens. - * + * * @var string */ private $name; - + /** * Miscellaneous, user-defined information about the queue. - * - * @var array|Metadata + * + * @var array|Metadata */ protected $metadata; - + /** - * Populated when the service's listQueues() method is called. Provides a + * Populated when the service's listQueues() method is called. Provides a * convenient link for a particular Queue.md. - * - * @var string + * + * @var string */ private $href; - + protected static $url_resource = 'queues'; protected static $json_collection_name = 'queues'; protected static $json_name = false; - + public $createKeys = array('name'); /** @@ -76,6 +84,7 @@ public function setName($name) } $this->name = $name; + return $this; } @@ -86,33 +95,6 @@ public function getName() { return $this->name; } - - /** - * Sets the metadata for this Queue. - * - * @param $data - * @return $this - * @throws \OpenCloud\Common\Exceptions\InvalidArgumentError - */ - public function setMetadata($data) - { - // Check for either objects, arrays or Metadata objects - if ($data instanceof Metadata) { - $metadata = $data; - } elseif (is_array($data) || is_object($data)) { - $metadata = new Metadata(); - $metadata->setArray($data); - } else { - throw new InvalidArgumentError(sprintf( - 'You must specify either an array/object of parameters, or an ' - . 'instance of Metadata. You provided: %s', - print_r($data, true) - )); - } - - $this->metadata = $metadata; - return $this; - } /** * Save this metadata both to the local object and the API. @@ -130,16 +112,6 @@ public function saveMetadata(array $params = array()) return $this->getClient()->put($this->getUrl('metadata'), self::getJsonHeader(), $json)->send(); } - - /** - * Returns the metadata associated with a Queue.md. - * - * @return Metadata|null - */ - public function getMetadata() - { - return $this->metadata; - } /** * Retrieve metadata from the API and set it to the local object. @@ -175,11 +147,11 @@ public function update($params = array()) { return $this->noUpdate(); } - + /** - * This operation returns queue statistics including how many messages are + * This operation returns queue statistics including how many messages are * in the queue, broken out by status. - * + * * @return object */ public function getStats() @@ -189,17 +161,17 @@ public function getStats() ->send(); $body = Formatter::decode($response); - + return (!isset($body->messages)) ? false : $body->messages; } /** - * Gets a message either by a specific ID, or, if no ID is specified, just + * Gets a message either by a specific ID, or, if no ID is specified, just * an empty Message object. - * - * @param string|null $id If a string, then the service will retrieve an - * individual message based on its specific ID. If NULL, then an empty - * object is returned for further use. + * + * @param string|null $id If a string, then the service will retrieve an + * individual message based on its specific ID. If NULL, then an empty + * object is returned for further use. * @return Message */ public function getMessage($id = null) @@ -227,60 +199,59 @@ public function createMessage(array $params) public function createMessages(array $messages) { $objects = array(); - + foreach ($messages as $dataArray) { $objects[] = $this->getMessage($dataArray)->createJson(); } - + $json = json_encode(array_slice($objects, 0, self::MAX_POST_MESSAGES)); $this->checkJsonError(); - + $response = $this->getClient() ->post($this->getUrl('messages'), self::getJsonHeader(), $json) ->send(); if (null !== ($location = $response->getHeader('Location'))) { - $parts = array_merge($this->getUrl()->getParts(), parse_url($location)); $url = Url::factory(Url::buildUrl($parts)); $options = $this->makeResourceIteratorOptions(__NAMESPACE__ . '\\Message') + array( - 'baseUrl' => $url, - 'limit.page' => 10 - ); + 'baseUrl' => $url, + 'limit.page' => 10 + ); return PaginatedIterator::factory($this, $options); } - + return true; } - + /** - * Lists messages according to certain filter options. Results are ordered + * Lists messages according to certain filter options. Results are ordered * by age, oldest message first. All of the parameters are optional. - * + * * @param array $options An associative array of filtering parameters: - * + * * - ids (array) A two-dimensional array of IDs to retrieve. - * + * * - claim_id (string) The claim ID to which the message is associated. - * - * - marker (string) An opaque string that the client can use to request the - * next batch of messages. If not specified, the API will return all + * + * - marker (string) An opaque string that the client can use to request the + * next batch of messages. If not specified, the API will return all * messages at the head of the queue (up to limit). - * - * - limit (integer) A number up to 20 (the default, but is configurable) + * + * - limit (integer) A number up to 20 (the default, but is configurable) * queues to return. If not specified, it defaults to 10. - * - * - echo (bool) Determines whether the API returns a client's own messages, - * as determined by the uuid portion of the User-Agent header. If not + * + * - echo (bool) Determines whether the API returns a client's own messages, + * as determined by the uuid portion of the User-Agent header. If not * specified, echo defaults to FALSE. - * - * - include_claimed (bool) Determines whether the API returns claimed - * messages as well as unclaimed messages. If not specified, defaults + * + * - include_claimed (bool) Determines whether the API returns claimed + * messages as well as unclaimed messages. If not specified, defaults * to FALSE (i.e. only unclaimed messages are returned). - * - * @return Collection + * + * @return \OpenCloud\Queues\Collection\MessageIterator */ public function listMessages(array $options = array()) { @@ -288,7 +259,17 @@ public function listMessages(array $options = array()) if (isset($options['ids']) && is_array($options['ids'])) { $options['ids'] = implode(',', $options['ids']); } - + + // PHP will cast boolean values to integers (true => 1, false => 0) but + // the Queues REST API expects strings as query parameters ("true" and "false"). + foreach ($options as $optionKey => $optionValue) { + if (true === $optionValue) { + $options[$optionKey] = "true"; + } elseif (false === $optionValue) { + $options[$optionKey] = "false"; + } + } + $url = $this->getUrl('messages', $options); $options = $this->makeResourceIteratorOptions(__NAMESPACE__ . '\\Message') + array( @@ -296,13 +277,13 @@ public function listMessages(array $options = array()) 'limit.page' => 10 ); - return PaginatedIterator::factory($this, $options); + return MessageIterator::factory($this, $options); } - + /** - * This operation immediately deletes the specified messages, providing a + * This operation immediately deletes the specified messages, providing a * means for bulk deletes. - * + * * @param array $ids Two-dimensional array of IDs to be deleted * @return boolean */ @@ -310,30 +291,31 @@ public function deleteMessages(array $ids) { $url = $this->url('messages', array('ids' => implode(',', $ids))); $this->getClient()->delete($url)->send(); + return true; } /** - * This operation claims a set of messages, up to limit, from oldest to - * newest, skipping any that are already claimed. If no unclaimed messages + * This operation claims a set of messages, up to limit, from oldest to + * newest, skipping any that are already claimed. If no unclaimed messages * are available, FALSE is returned. - * - * You should delete the message when you have finished processing it, - * before the claim expires, to ensure the message is processed only once. + * + * You should delete the message when you have finished processing it, + * before the claim expires, to ensure the message is processed only once. * Be aware that you must call the delete() method on the Message object and - * pass in the Claim ID, rather than relying on the service's bulk delete - * deleteMessages() method. This is so that the server can return an error - * if the claim just expired, giving you a chance to roll back your processing - * of the given message, since another worker will likely claim the message + * pass in the Claim ID, rather than relying on the service's bulk delete + * deleteMessages() method. This is so that the server can return an error + * if the claim just expired, giving you a chance to roll back your processing + * of the given message, since another worker will likely claim the message * and process it. - * - * Just as with a message's age, the age given for the claim is relative to - * the server's clock, and is useful for determining how quickly messages are + * + * Just as with a message's age, the age given for the claim is relative to + * the server's clock, and is useful for determining how quickly messages are * getting processed, and whether a given message's claim is about to expire. - * - * When a claim expires, it is removed, allowing another client worker to + * + * When a claim expires, it is removed, allowing another client worker to * claim the message in the case that the original worker fails to process it. - * + * * @param int $limit */ public function claimMessages(array $options = array()) @@ -346,7 +328,7 @@ public function claimMessages(array $options = array()) 'grace' => $grace, 'ttl' => $ttl )); - + $url = $this->getUrl('claims', array('limit' => $limit)); $response = $this->getClient()->post($url, self::getJsonHeader(), $json)->send(); @@ -359,11 +341,11 @@ public function claimMessages(array $options = array()) return PaginatedIterator::factory($this, $options, Formatter::decode($response)); } - + /** * If an ID is supplied, the API is queried for a persistent object; otherwise * an empty object is returned. - * + * * @param int $id * @return Claim */ @@ -371,5 +353,4 @@ public function getClaim($id = null) { return $this->getService()->resource('Claim', $id, $this); } - -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Queues/Service.php b/rackspace/php-opencloud/lib/OpenCloud/Queues/Service.php index 9b81d62a9..506555b4e 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Queues/Service.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Queues/Service.php @@ -1,10 +1,18 @@ + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Queues; @@ -13,44 +21,45 @@ use Guzzle\Http\Exception\BadResponseException; use OpenCloud\Common\Exceptions\InvalidArgumentError; use OpenCloud\Common\Service\CatalogService; +use OpenCloud\Queues\Resource\Queue; use Symfony\Component\EventDispatcher\EventSubscriberInterface; /** - * Cloud Queues is an open source, scalable, and highly available message and - * notifications service. Users of this service can create and manage a - * "producer-consumer" or a "publisher-subscriber" model from one simple API. It + * Cloud Queues is an open source, scalable, and highly available message and + * notifications service. Users of this service can create and manage a + * "producer-consumer" or a "publisher-subscriber" model from one simple API. It * is made up of a few basic components: queues, messages, claims, and stats. - * - * In the producer-consumer model, users create queues where producers - * can post messages. Workers, or consumers, may then claim those messages and - * delete them once complete. A single claim may contain multiple messages, and + * + * In the producer-consumer model, users create queues where producers + * can post messages. Workers, or consumers, may then claim those messages and + * delete them once complete. A single claim may contain multiple messages, and * administrators are given the ability to query claims for status. - * - * In the publisher-subscriber model, messages are posted to a queue like above, - * but messages are never claimed. Instead, subscribers, or watchers, simply - * send GET requests to pull all messages since their last request. In this - * model, a message will remain in the queue, unclaimed, until the message's + * + * In the publisher-subscriber model, messages are posted to a queue like above, + * but messages are never claimed. Instead, subscribers, or watchers, simply + * send GET requests to pull all messages since their last request. In this + * model, a message will remain in the queue, unclaimed, until the message's * time to live (TTL) has expired. - * + * * Here is an overview of the Cloud Queues workflow: - * + * * 1. You create a queue to which producers post messages. - * + * * 2. Producers post messages to the queue. - * - * 3. Workers claim messages from the queue, complete the work in that message, + * + * 3. Workers claim messages from the queue, complete the work in that message, * and delete the message. - * - * 4. If a worker plans to be offline before its message completes, the worker - * can retire the claim TTL, putting the message back into the queue for + * + * 4. If a worker plans to be offline before its message completes, the worker + * can retire the claim TTL, putting the message back into the queue for * another worker to claim. - * - * 5. Subscribers monitor the claims of these queues to keep track of activity + * + * 5. Subscribers monitor the claims of these queues to keep track of activity * and help troubleshoot if things go wrong. * - * For the majority of use cases, Cloud Queues itself will not be responsible - * for the ordering of messages. If there is only a single producer, however, - * Cloud Queueing guarantees that messages are handled in a First In, First Out + * For the majority of use cases, Cloud Queues itself will not be responsible + * for the ordering of messages. If there is only a single producer, however, + * Cloud Queueing guarantees that messages are handled in a First In, First Out * (FIFO) order. */ class Service extends CatalogService implements EventSubscriberInterface @@ -66,21 +75,21 @@ public static function getSubscribedEvents() } /** - * Append the Client-ID header to all requests for this service. + * Set the Client-ID header to all requests for this service. * * @param Event $event */ public function appendClientIdToRequest(Event $event) { - $event['request']->addHeader('Client-ID', $this->getClientId()); + $event['request']->setHeader('Client-ID', $this->getClientId()); } /** * An arbitrary string used to differentiate your worker/subscriber. This is * needed, for example, when you return back a list of messages and want to * know the ones your worker is processing. - * - * @var string + * + * @var string */ private $clientId; @@ -94,6 +103,7 @@ public function setClientId($clientId = null) $clientId = self::generateUuid(); } $this->clientId = $clientId; + return $this; } @@ -108,7 +118,10 @@ public function getClientId() /** * Create a new Queue. * - * @param $name Name of the new queue + * @param string $name Name of the new queue + * + * @throws InvalidArgumentError + * * @return Queue */ public function createQueue($name) @@ -130,41 +143,41 @@ public function createQueue($name) } /** - * This operation lists queues for the project, sorting the queues + * This operation lists queues for the project, sorting the queues * alphabetically by name. - * + * * @param array $params An associative array of optional parameters: - * - * - marker (string) Specifies the name of the last queue received in a - * previous request, or none to get the first page of + * + * - marker (string) Specifies the name of the last queue received in a + * previous request, or none to get the first page of * results. Optional. - * - * - limit (integer) Specifies up to 20 (the default, but configurable) + * + * - limit (integer) Specifies up to 20 (the default, but configurable) * queues to return. Optional. - * - * - detailed (bool) Determines whether queue metadata is included in the + * + * - detailed (bool) Determines whether queue metadata is included in the * response. Optional. - * + * * @return Collection */ public function listQueues(array $params = array()) { return $this->resourceList('Queue', $this->getUrl('queues', $params)); } - + /** * Return an empty Queue.md object. - * + * * @return Queue */ public function getQueue($id = null) { return $this->resource('Queue', $id); } - + /** * This operation checks to see if the specified queue exists. - * + * * @param string $name The queue name that you want to check * @return bool */ @@ -176,7 +189,7 @@ public function hasQueue($name) print_r($name, true) )); } - + try { $url = $this->getUrl(); $url->addPath('queues')->addPath($name); @@ -186,7 +199,6 @@ public function hasQueue($name) return true; } catch (BadResponseException $e) { return false; - } + } } - -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Rackspace.php b/rackspace/php-opencloud/lib/OpenCloud/Rackspace.php index 78853d392..40473a4b0 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Rackspace.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Rackspace.php @@ -1,18 +1,24 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud; use OpenCloud\Common\Exceptions\CredentialError; use OpenCloud\Common\Service\ServiceBuilder; -use OpenCloud\Identity\Service as IdentityService; /** * Rackspace extends the OpenStack class with support for Rackspace's @@ -48,7 +54,6 @@ public function getCredentials() $secret = $this->getSecret(); if (!empty($secret['username']) && !empty($secret['apiKey'])) { - $credentials = array('auth' => array( 'RAX-KSKEY:apiKeyCredentials' => array( 'username' => $secret['username'], @@ -63,7 +68,6 @@ public function getCredentials() } return json_encode($credentials); - } else { throw new CredentialError('Unrecognized credential secret'); } @@ -80,8 +84,8 @@ public function getCredentials() public function databaseService($name = null, $region = null, $urltype = null) { return ServiceBuilder::factory($this, 'OpenCloud\Database\Service', array( - 'name' => $name, - 'region' => $region, + 'name' => $name, + 'region' => $region, 'urlType' => $urltype )); } @@ -97,8 +101,8 @@ public function databaseService($name = null, $region = null, $urltype = null) public function loadBalancerService($name = null, $region = null, $urltype = null) { return ServiceBuilder::factory($this, 'OpenCloud\LoadBalancer\Service', array( - 'name' => $name, - 'region' => $region, + 'name' => $name, + 'region' => $region, 'urlType' => $urltype )); } @@ -114,8 +118,8 @@ public function loadBalancerService($name = null, $region = null, $urltype = nul public function dnsService($name = null, $region = null, $urltype = null) { return ServiceBuilder::factory($this, 'OpenCloud\DNS\Service', array( - 'name' => $name, - 'region' => $region, + 'name' => $name, + 'region' => $region, 'urlType' => $urltype )); } @@ -131,8 +135,8 @@ public function dnsService($name = null, $region = null, $urltype = null) public function cloudMonitoringService($name = null, $region = null, $urltype = null) { return ServiceBuilder::factory($this, 'OpenCloud\CloudMonitoring\Service', array( - 'name' => $name, - 'region' => $region, + 'name' => $name, + 'region' => $region, 'urlType' => $urltype )); } @@ -148,12 +152,12 @@ public function cloudMonitoringService($name = null, $region = null, $urltype = public function autoscaleService($name = null, $region = null, $urltype = null) { return ServiceBuilder::factory($this, 'OpenCloud\Autoscale\Service', array( - 'name' => $name, - 'region' => $region, + 'name' => $name, + 'region' => $region, 'urlType' => $urltype )); } - + /** * Creates a new CloudQueues service. Note: this is a Rackspace-only feature. * @@ -165,10 +169,28 @@ public function autoscaleService($name = null, $region = null, $urltype = null) public function queuesService($name = null, $region = null, $urltype = null) { return ServiceBuilder::factory($this, 'OpenCloud\Queues\Service', array( - 'name' => $name, - 'region' => $region, + 'name' => $name, + 'region' => $region, + 'urlType' => $urltype + )); + } + + /** + * Creates a new CDN (Rackspace CDN) service object + * + * @param string $name The name of the service as it appears in the Catalog + * @param string $region The region (DFW, IAD, ORD, LON, SYD) + * @param string $urltype The URL type ("publicURL" or "internalURL") + * @return \OpenCloud\Cdn\Service + * @codeCoverageIgnore + */ + public function cdnService($name = null, $region = null, $urltype = null) + { + return ServiceBuilder::factory($this, 'OpenCloud\CDN\Service', array( + 'name' => $name, + 'type' => 'rax:cdn', + 'region' => $region, 'urlType' => $urltype )); } - } diff --git a/rackspace/php-opencloud/lib/OpenCloud/Version.php b/rackspace/php-opencloud/lib/OpenCloud/Version.php index 440ac4372..2072ec8ec 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Version.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Version.php @@ -1,25 +1,33 @@ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud; -use Guzzle\Http\Curl\CurlVersion; use Guzzle\Common\Version as GuzzleVersion; +use Guzzle\Http\Curl\CurlVersion; /** * Class Version * * @package OpenCloud */ -class Version +class Version { - const VERSION = '1.9.2'; + const VERSION = '1.14.2'; /** * @return string Indicate current SDK version. @@ -44,5 +52,4 @@ public static function getGuzzleVersion() { return GuzzleVersion::VERSION; } - -} \ No newline at end of file +} diff --git a/rackspace/php-opencloud/lib/OpenCloud/Volume/Resource/Snapshot.php b/rackspace/php-opencloud/lib/OpenCloud/Volume/Resource/Snapshot.php index 500489933..9c27c3ccf 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Volume/Resource/Snapshot.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Volume/Resource/Snapshot.php @@ -1,25 +1,29 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Volume\Resource; -use OpenCloud\Common\PersistentObject; -use OpenCloud\Common\Lang; -use OpenCloud\Common\Exceptions; +use OpenCloud\Common\Resource\PersistentResource; /** * The Snapshot class represents a single block storage snapshot */ -class Snapshot extends PersistentObject +class Snapshot extends PersistentResource { - public $id; public $display_name; public $display_description; @@ -41,14 +45,46 @@ class Snapshot extends PersistentObject 'force' ); - public function update($params = array()) + /** + * Rename either the `display_description` or the `display_name` properties + * + * @param array $params + * @return \Guzzle\Http\Message\Response + * @throws \InvalidArgumentException + */ + public function rename(array $params = array()) + { + $data = array(); + + $keys = array('display_description' => true, 'display_name' => true); + + foreach ($params as $key => $value) { + if (isset($keys[$key])) { + $data[$key] = $value; + } else { + throw new \InvalidArgumentException(sprintf( + 'You cannot update the %s snapshot property. Valid keys are: %s', + $key, implode($keys, ',') + )); + } + } + + $json = json_encode(array( + 'snapshot' => $data + )); + + return $this->getClient() + ->put($this->getUrl(), self::getJsonHeader(), $json) + ->send(); + } + + public function update($params = array()) { return $this->noUpdate(); } - public function name() + public function name() { return $this->display_name; } - } diff --git a/rackspace/php-opencloud/lib/OpenCloud/Volume/Resource/Volume.php b/rackspace/php-opencloud/lib/OpenCloud/Volume/Resource/Volume.php index 6190b54f8..73ef916ce 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Volume/Resource/Volume.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Volume/Resource/Volume.php @@ -1,25 +1,31 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Volume\Resource; -use OpenCloud\Common\PersistentObject; -use OpenCloud\Common\Lang; use OpenCloud\Common\Exceptions; +use OpenCloud\Common\Lang; +use OpenCloud\Common\Resource\PersistentResource; /** * The Volume class represents a single block storage volume */ -class Volume extends PersistentObject +class Volume extends PersistentResource { - public $id; public $status; public $display_name; @@ -31,7 +37,10 @@ class Volume extends PersistentObject public $snapshot_id; public $attachments = array(); public $created_at; - + public $source_volid; + public $imageRef; + public $bootable; + protected static $json_name = 'volume'; protected static $url_resource = 'volumes'; @@ -41,39 +50,68 @@ class Volume extends PersistentObject 'display_description', 'size', 'volume_type', - 'availability_zone' + 'availability_zone', + 'metadata', + 'source_volid', + 'bootable', + 'imageRef' ); - // Normally we'd populate a sibling object when this one refreshes - // but there are times (i.e. during creation) when the NAME of the VolumeType - // is returned, instead of its primary key... - protected $associatedResources = array( - //'volume_type' => 'VolumeType' - ); - - public function update($params = array()) + protected $associatedResources = array(); + + public function update($params = array()) { throw new Exceptions\UpdateError( Lang::translate('Block storage volumes cannot be updated') ); } - public function name() + /** + * Rename either the `display_description` or the `display_name` properties + * + * @param array $params + * @return \Guzzle\Http\Message\Response + * @throws \InvalidArgumentException + */ + public function rename(array $params = array()) + { + $data = array(); + + $keys = array('display_description', 'display_name'); + + foreach ($params as $key => $value) { + if (in_array($key, $keys)) { + $data[$key] = $value; + } else { + throw new \InvalidArgumentException(sprintf( + 'You cannot update the %s volume property. Valid keys are: %s', + $key, implode($keys, ',') + )); + } + } + + $json = json_encode(array( + 'volume' => $data + )); + + return $this->getClient() + ->put($this->getUrl(), self::getJsonHeader(), $json) + ->send(); + } + + public function name() { return $this->display_name; } - protected function createJson() + protected function createJson() { $element = parent::createJson(); - if ($this->propertyExists('volume_type') - && $this->getProperty('volume_type') instanceof VolumeType - ) { + if ($this->getProperty('volume_type') instanceof VolumeType) { $element->volume->volume_type = $this->volume_type->name(); } return $element; } - } diff --git a/rackspace/php-opencloud/lib/OpenCloud/Volume/Resource/VolumeType.php b/rackspace/php-opencloud/lib/OpenCloud/Volume/Resource/VolumeType.php index bead0e64e..a975bbde0 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Volume/Resource/VolumeType.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Volume/Resource/VolumeType.php @@ -1,25 +1,29 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Volume\Resource; -use OpenCloud\Common\PersistentObject; -use OpenCloud\Common\Lang; -use OpenCloud\Common\Exceptions; +use OpenCloud\Common\Resource\PersistentResource; /** * The VolumeType class represents a single block storage volume type */ -class VolumeType extends PersistentObject +class VolumeType extends PersistentResource { - public $id; public $name; public $extra_specs; @@ -27,40 +31,18 @@ class VolumeType extends PersistentObject protected static $json_name = 'volume_type'; protected static $url_resource = 'types'; - /** - * Creates are not permitted - * - * @throws OpenCloud\CreateError always - */ - public function Create($params = array()) + public function create($params = array()) { - throw new Exceptions\CreateError( - Lang::translate('VolumeType cannot be created') - ); + return $this->noCreate(); } - /** - * updates are not permitted - * - * @throws OpenCloud\UpdateError always - */ - public function Update($params = array()) + public function update($params = array()) { - throw new Exceptions\UpdateError( - Lang::translate('VolumeType cannot be updated') - ); + return $this->noUpdate(); } - /** - * deletes are not permitted - * - * @throws OpenCloud\DeleteError - */ - public function Delete() + public function delete() { - throw new Exceptions\DeleteError( - Lang::translate('VolumeType cannot be deleted') - ); + return $this->noDelete(); } - } diff --git a/rackspace/php-opencloud/lib/OpenCloud/Volume/Service.php b/rackspace/php-opencloud/lib/OpenCloud/Volume/Service.php index c1abb4696..689e65cea 100644 --- a/rackspace/php-opencloud/lib/OpenCloud/Volume/Service.php +++ b/rackspace/php-opencloud/lib/OpenCloud/Volume/Service.php @@ -1,96 +1,105 @@ - * @author Jamie Hannaford + * Copyright 2012-2014 Rackspace US, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ namespace OpenCloud\Volume; -use OpenCloud\OpenStack; use OpenCloud\Common\Service\NovaService; +use OpenCloud\Volume\Resource\Snapshot; class Service extends NovaService { const DEFAULT_TYPE = 'volume'; const DEFAULT_NAME = 'cloudBlockStorage'; - /** - * Returns a Volume object - * - * @api - * @param string $id the Volume ID - * @return Resource\Volume - */ - public function volume($id = null) - { - return new Resource\Volume($this, $id); - } + /** + * Returns a Volume object + * + * @param string $id the Volume ID + * @return Resource\Volume + */ + public function volume($id = null) + { + return $this->resource('Volume', $id); + } - /** - * Returns a Collection of Volume objects - * - * @api - * @param boolean $details if TRUE, return all details - * @param array $filter array of filter key/value pairs - * @return \OpenCloud\Common\Collection - */ - public function volumeList($details = true, $filter = array()) - { - $url = clone $this->getUrl(Resource\Volume::ResourceName()); - if ($details) { + /** + * Returns a Collection of Volume objects + * + * @param boolean $details if TRUE, return all details + * @param array $filter array of filter key/value pairs + * @return \OpenCloud\Common\Collection + */ + public function volumeList($details = true, $filter = array()) + { + $url = clone $this->getUrl(Resource\Volume::ResourceName()); + + if ($details === true) { $url->addPath('detail'); } - return $this->collection('OpenCloud\Volume\Resource\Volume', $url); - } - /** - * Returns a VolumeType object - * - * @api - * @param string $id the VolumeType ID - * @return Resource\Volume - */ - public function volumeType($id = null) - { - return new Resource\VolumeType($this, $id); - } + $url->setQuery($filter); + + return $this->resourceList('Volume', $url); + } + + /** + * Returns a VolumeType object + * + * @param string $id the VolumeType ID + * @return Resource\Volume + */ + public function volumeType($id = null) + { + return $this->resource('VolumeType', $id); + } - /** - * Returns a Collection of VolumeType objects - * - * @api - * @param array $filter array of filter key/value pairs - * @return \OpenCloud\Common\Collection - */ - public function volumeTypeList($filter = array()) - { - return $this->collection('\OpenCloud\Volume\Resource\VolumeType'); - } + /** + * Returns a Collection of VolumeType objects + * + * @param array $filter array of filter key/value pairs + * @return \OpenCloud\Common\Collection + */ + public function volumeTypeList($filter = array()) + { + return $this->resourceList('VolumeType'); + } - /** - * returns a Snapshot object associated with this volume - * - * @return Resource\Snapshot - */ - public function snapshot($id = null) - { - return new Resource\Snapshot($this, $id); - } + /** + * Returns a Snapshot object associated with this volume + * + * @param null $id + * @return Resource\Snapshot + */ + public function snapshot($id = null) + { + return $this->resource('Snapshot', $id); + } - /** - * Returns a Collection of Snapshot objects - * - * @api - * @param array $filter array of filter key/value pairs - * @return \OpenCloud\Common\Collection - */ - public function snapshotList($filter = array()) - { - return $this->collection('OpenCloud\Volume\Resource\Snapshot'); - } + /** + * Returns a Collection of Snapshot objects + * + * @param array $filter array of filter key/value pairs + * @return \OpenCloud\Common\Collection + */ + public function snapshotList($filter = array()) + { + $url = clone $this->getUrl(); + $url->addPath(Snapshot::resourceName())->setQuery($filter); + return $this->resourceList('Snapshot', $url); + } } diff --git a/rackspace/php-opencloud/lib/php-opencloud.php b/rackspace/php-opencloud/lib/php-opencloud.php index 0ecc16b87..1fda2f0c4 100644 --- a/rackspace/php-opencloud/lib/php-opencloud.php +++ b/rackspace/php-opencloud/lib/php-opencloud.php @@ -1,3 +1,18 @@ + + + + +