1<?php
2namespace Aws;
3
4use Aws\Endpoint\PartitionEndpointProvider;
5use Aws\Endpoint\PartitionInterface;
6
7class MultiRegionClient implements AwsClientInterface
8{
9    use AwsClientTrait;
10
11    /** @var AwsClientInterface[] A pool of clients keyed by region. */
12    private $clientPool = [];
13    /** @var callable */
14    private $factory;
15    /** @var PartitionInterface */
16    private $partition;
17    /** @var array */
18    private $args;
19    /** @var array */
20    private $config;
21    /** @var HandlerList */
22    private $handlerList;
23    /** @var array */
24    private $aliases;
25
26    public static function getArguments()
27    {
28        $args = array_intersect_key(
29            ClientResolver::getDefaultArguments(),
30            ['service' => true, 'region' => true]
31        );
32        $args['region']['required'] = false;
33
34        return $args + [
35            'client_factory' => [
36                'type' => 'config',
37                'valid' => ['callable'],
38                'doc' => 'A callable that takes an array of client'
39                    . ' configuration arguments and returns a regionalized'
40                    . ' client.',
41                'required' => true,
42                'internal' => true,
43                'default' => function (array $args) {
44                    $namespace = manifest($args['service'])['namespace'];
45                    $klass = "Aws\\{$namespace}\\{$namespace}Client";
46                    $region = isset($args['region']) ? $args['region'] : null;
47
48                    return function (array $args) use ($klass, $region) {
49                        if ($region && empty($args['region'])) {
50                            $args['region'] = $region;
51                        }
52
53                        return new $klass($args);
54                    };
55                },
56            ],
57            'partition' => [
58                'type'    => 'config',
59                'valid'   => ['string', PartitionInterface::class],
60                'doc'     => 'AWS partition to connect to. Valid partitions'
61                    . ' include "aws," "aws-cn," and "aws-us-gov." Used to'
62                    . ' restrict the scope of the mapRegions method.',
63                'default' => function (array $args) {
64                    $region = isset($args['region']) ? $args['region'] : '';
65                    return PartitionEndpointProvider::defaultProvider()
66                        ->getPartition($region, $args['service']);
67                },
68                'fn'      => function ($value, array &$args) {
69                    if (is_string($value)) {
70                        $value = PartitionEndpointProvider::defaultProvider()
71                            ->getPartitionByName($value);
72                    }
73
74                    if (!$value instanceof PartitionInterface) {
75                        throw new \InvalidArgumentException('No valid partition'
76                            . ' was provided. Provide a concrete partition or'
77                            . ' the name of a partition (e.g., "aws," "aws-cn,"'
78                            . ' or "aws-us-gov").'
79                        );
80                    }
81
82                    $args['partition'] = $value;
83                    $args['endpoint_provider'] = $value;
84                }
85            ],
86        ];
87    }
88
89    /**
90     * The multi-region client constructor accepts the following options:
91     *
92     * - client_factory: (callable) An optional callable that takes an array of
93     *   client configuration arguments and returns a regionalized client.
94     * - partition: (Aws\Endpoint\Partition|string) AWS partition to connect to.
95     *   Valid partitions include "aws," "aws-cn," and "aws-us-gov." Used to
96     *   restrict the scope of the mapRegions method.
97     * - region: (string) Region to connect to when no override is provided.
98     *   Used to create the default client factory and determine the appropriate
99     *   AWS partition when present.
100     *
101     * @param array $args Client configuration arguments.
102     */
103    public function __construct(array $args = [])
104    {
105        if (!isset($args['service'])) {
106            $args['service'] = $this->parseClass();
107        }
108
109        $this->handlerList = new HandlerList(function (
110            CommandInterface $command
111        ) {
112            list($region, $args) = $this->getRegionFromArgs($command->toArray());
113            $command = $this->getClientFromPool($region)
114                ->getCommand($command->getName(), $args);
115            return $this->executeAsync($command);
116        });
117
118        $argDefinitions = static::getArguments();
119        $resolver = new ClientResolver($argDefinitions);
120        $args = $resolver->resolve($args, $this->handlerList);
121        $this->config = $args['config'];
122        $this->factory = $args['client_factory'];
123        $this->partition = $args['partition'];
124        $this->args = array_diff_key($args, $args['config']);
125    }
126
127    /**
128     * Get the region to which the client is configured to send requests by
129     * default.
130     *
131     * @return string
132     */
133    public function getRegion()
134    {
135        return $this->getClientFromPool()->getRegion();
136    }
137
138    /**
139     * Create a command for an operation name.
140     *
141     * Special keys may be set on the command to control how it behaves,
142     * including:
143     *
144     * - @http: Associative array of transfer specific options to apply to the
145     *   request that is serialized for this command. Available keys include
146     *   "proxy", "verify", "timeout", "connect_timeout", "debug", "delay", and
147     *   "headers".
148     * - @region: The region to which the command should be sent.
149     *
150     * @param string $name Name of the operation to use in the command
151     * @param array  $args Arguments to pass to the command
152     *
153     * @return CommandInterface
154     * @throws \InvalidArgumentException if no command can be found by name
155     */
156    public function getCommand($name, array $args = [])
157    {
158        return new Command($name, $args, clone $this->getHandlerList());
159    }
160
161    public function getConfig($option = null)
162    {
163        if (null === $option) {
164            return $this->config;
165        }
166
167        if (isset($this->config[$option])) {
168            return $this->config[$option];
169        }
170
171        return $this->getClientFromPool()->getConfig($option);
172    }
173
174    public function getCredentials()
175    {
176        return $this->getClientFromPool()->getCredentials();
177    }
178
179    public function getHandlerList()
180    {
181        return $this->handlerList;
182    }
183
184    public function getApi()
185    {
186        return $this->getClientFromPool()->getApi();
187    }
188
189    public function getEndpoint()
190    {
191        return $this->getClientFromPool()->getEndpoint();
192    }
193
194    /**
195     * @param string $region    Omit this argument or pass in an empty string to
196     *                          allow the configured client factory to apply the
197     *                          region.
198     *
199     * @return AwsClientInterface
200     */
201    protected function getClientFromPool($region = '')
202    {
203        if (empty($this->clientPool[$region])) {
204            $factory = $this->factory;
205            $this->clientPool[$region] = $factory(
206                array_replace($this->args, array_filter(['region' => $region]))
207            );
208        }
209
210        return $this->clientPool[$region];
211    }
212
213    /**
214     * Parse the class name and return the "service" name of the client.
215     *
216     * @return string
217     */
218    private function parseClass()
219    {
220        $klass = get_class($this);
221
222        if ($klass === __CLASS__) {
223            return '';
224        }
225
226        return strtolower(substr($klass, strrpos($klass, '\\') + 1, -17));
227    }
228
229    private function getRegionFromArgs(array $args)
230    {
231        $region = isset($args['@region'])
232            ? $args['@region']
233            : $this->getRegion();
234        unset($args['@region']);
235
236        return [$region, $args];
237    }
238}
239