Skip to content
This repository has been archived by the owner on Apr 17, 2021. It is now read-only.

Commit

Permalink
Fixed inheritance issues with Guzzle/AWS. Extends now works. Ensure a…
Browse files Browse the repository at this point in the history
… class is always set.
  • Loading branch information
jmcclell committed Feb 7, 2014
1 parent ac74442 commit 85cfd2a
Show file tree
Hide file tree
Showing 10 changed files with 1,464 additions and 110 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/vendor
phpunit.xml
build
.DS_Store
51 changes: 51 additions & 0 deletions Aws/Common/Aws.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

namespace JLM\AwsBundle\Aws\Common;


use Aws\Common\Aws as BaseAws;
use Guzzle\Service\Builder\ServiceBuilderLoader;

/**
* The base AWS class provides no way to load a full configuration
* from an array. It forces you to load from a file. That is not
* really feasible in our case so we are simply overriding the
* factory method to compensate for that decision on their part.
*/
class Aws extends BaseAws
{
/**
* Create a new service locator for the AWS SDK
*
* You can configure the service locator is four different ways:
*
* 1. Use the default configuration file shipped with the SDK that wires class names with service short names and
* specify global parameters to add to every definition (e.g. key, secret, credentials, etc)
*
* 2. Use a custom configuration file that extends the default config and supplies credentials for each service.
*
* 3. Use a custom config file that wires services to custom short names for services.
*
* 4. If you are on Amazon EC2, you can use the default configuration file and not provide any credentials so that
* you are using InstanceProfile credentials.
*
* @param array|string $config The full path to a .php or .js|.json file, or an associative array of data
* of configuration parameters.
* @param array $globalParameters Global parameters to pass to every service as it is instantiated.
*
* @return Aws
*/
public static function factory($config = null, array $globalParameters = array())
{
if (empty($config)) {
// If nothing is passed in, then use the default configuration file with credentials from the environment
$config = self::getDefaultServiceDefinition();
}

$loader = new ServiceBuilderLoader();
$loader->addAlias('_aws', static::getDefaultServiceDefinition())
->addAlias('_sdk1', __DIR__ . '/Resources/sdk1-config.php');

return $loader->load($config, $globalParameters);
}
}
123 changes: 95 additions & 28 deletions Config/AwsConfigTranslator.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
namespace JLM\AwsBundle\Config;

/**
* Utility class which aids in transforming the JLMAwsBundle's configuration
* Utility class which transforms the JLMAwsBundle's configuration
* into an AWS configuration array that can be passed directly to the
* AWS service factory.
* AWS service builder.
*
* Translation happens both in terms of array structure and key names.
*
Expand All @@ -21,7 +21,8 @@
* the values found in that config.
*
* Our 'default' service instances simply correspond to the services that are already in this config. All
* other service instances are added and will extend one of these default instances, by default.
* other service instances are added and will extend one of these default instances, unless
* specified otherwise in the configuration.
*/
class AwsConfigTranslator
{
Expand Down Expand Up @@ -65,34 +66,37 @@ class AwsConfigTranslator
'autoscaling' => 'AutoScaling',
'cloudformation' => 'CloudFormation',
'cloudfront' => 'CloudFront',
'cloudfront_20120505' => 'CloudFront',
'cloudsearch' => 'CloudSearch',
'cloudtrail' => 'CloudTrail',
'cloudwatch' => 'CloudWatch',
'datapipeline' => 'DataPipeline',
'directconnect' => 'DirectConnect',
'dynamodb' => 'DynamoDb',
'dynamodb_20111205' => 'DynamoDb',
'elasticache' => 'ElastiCache',
'elasticbeanstalk' => 'ElasticBeanstalk',
'elasticloadbalancing' => 'ElasticLoadBalancing',
'elastictranscoder' => 'ElasticTranscoder',
'importexport' => 'ImportExport',
'opsworks' => 'OpsWorks',
'storagegateway' => 'StorageGateway',
'sdb' => 'SimpleDb'
'sdb' => 'SimpleDb',

);

/**
* [getAwsServiceName description]
* Translates an AWS service name to the spaceless service name (eg: dynamo_db -> dynamodb)
* @param string $name The service name per the bundle's configuration definition
* @return string The translated service name (AWS service provider look up name)
*/
public function translateToAwsServiceName($bundleConfigName)
public function translateToSpacelessAwsServiceName($bundleConfigName)
{
return preg_replace('/_([^0-9])/', '$1', $bundleConfigName); // remove underscores except for if followed by a number such as with cloud_front_20120505 which should become cloudfront_20120505
}

/**
* Returns the marketing name for a particular service, ie: the properly capitalized service
* Returns the proper name for a particular service, ie: the properly capitalized service
* name used for display. This name is also used as part of the class path for each service
* client.
*
Expand All @@ -101,14 +105,6 @@ public function translateToAwsServiceName($bundleConfigName)
*/
public function getProperAwsServiceName($serviceName)
{
// If the lookup name has an underscore, cut it off and anything after it. (eg: cloudfront_20120505)
$serviceNameParts = explode('_', $serviceName);
$serviceName = $serviceNameParts[0];

// If the lookup name has any periods, cut it off and anything after it. (eg: cloudfront.my_cloud_front)
$serviceNameParts = explode('.', $serviceName);
$serviceName = $serviceNameParts[0];

// Check if it's in the look up table, otherwise just capitalize the first letter
return (isset($this->properServiceNameLookupTable[$serviceName])) ?
$this->properServiceNameLookupTable[$serviceName] :
Expand All @@ -122,9 +118,14 @@ public function getProperAwsServiceName($serviceName)
* @param string $name The client service lookup name
* @return string The fully qualified class name
*/
public function getDefaultAwsClassByServiceName($name)
public function getDefaultAwsClassByServiceType($serviceType)
{
$properName = $this->getProperAwsServiceName($name);
// Remove anything after an underscore from service type
// so that we ge the base service type. eg: cloudfront rather than cloudfront_20120505
$parts = explode('_', $serviceType);
$serviceType = $parts[0];

$properName = $this->getProperAwsServiceName($serviceType);

return "Aws\\{$properName}\\{$properName}Client";
}
Expand Down Expand Up @@ -161,20 +162,24 @@ public function translateConfigToAwsConfig(array $config)
'services' => array()
);

$defaultSettings = array();
if(!empty($config['default_settings'])) {
$awsConfig['services']['default_settings']['params'] = $this->translateConfigBlock($config['default_settings']);
$defaultSettings = $this->translateConfigBlock($config['default_settings']);
$awsConfig['services']['default_settings']['params'] = $defaultSettings;
}

if(!empty($config['services'])) {
foreach($config['services'] as $serviceName => $serviceConfig) {
if (empty($serviceConfig)) {
foreach($config['services'] as $serviceType => $serviceInstancesConfig) {
if (empty($serviceInstancesConfig)) {
continue; // We only generate config for services that included configuration parameters
}
$awsConfig['services'] = array_merge($awsConfig['services'],
$this->translateServiceConfigBlock($serviceName, $serviceConfig));

$translatedConfigBlock = $this->translateServiceConfigBlock($serviceType, $serviceInstancesConfig, $defaultSettings);
$awsConfig['services'] = array_merge($awsConfig['services'], $translatedConfigBlock);
}
}

// die(json_encode($awsConfig, JSON_PRETTY_PRINT));
return $awsConfig;
}

Expand All @@ -190,7 +195,7 @@ public function translateConfigToAwsConfig(array $config)
* @param array $serviceConfig The bundle configuration for the service type
* @return array An AWS configuration array for the service type, containing all the instances for that type
*/
protected function translateServiceConfigBlock($serviceType, $serviceConfig)
protected function translateServiceConfigBlock($serviceType, $serviceConfig, $defaultSettings)
{
$awsServiceConfig = array();

Expand All @@ -208,14 +213,12 @@ protected function translateServiceConfigBlock($serviceType, $serviceConfig)
}


$translatedServiceType = $this->translateToAwsServiceName($serviceType);
$translatedServiceType = $this->translateToSpacelessAwsServiceName($serviceType);

if (isset($serviceInstanceConfig['extends'])) {
$translatedInstanceConfig['extends'] = $serviceType . '.' . $serviceInstanceConfig['extends'];
$translatedInstanceConfig['extends'] = $translatedServiceType . '.' . $serviceInstanceConfig['extends'];
} else {
if ($serviceInstanceName !== 'default') {
$translatedInstanceConfig['extends'] = $translatedServiceType;
}
$translatedInstanceConfig['extends'] = $translatedServiceType;
}

if ($serviceInstanceName === 'default') {
Expand All @@ -228,6 +231,7 @@ protected function translateServiceConfigBlock($serviceType, $serviceConfig)
$awsServiceConfig[$serviceInstanceName] = $translatedInstanceConfig;
}

$awsServiceConfig = $this->processInheritance($serviceType, $awsServiceConfig, $defaultSettings);
return $awsServiceConfig;
}

Expand Down Expand Up @@ -347,4 +351,67 @@ protected function normalizeAuthType($authType)
}
return $authType;
}

/**
* Guzzle's configuration inheritance is really limited.
*
* It only supports 1-level inheritance. Thus, if A is a parent of B, and B
* is a parent of C, then C does NOT get parameters defined in A, but rather
* only those explicitly defined in B. So, we have to process our array and
* do a lot of copying to overcome this. It's worth it, though, as it makes
* our configuration much more extensible.
*
* This method also ensures all services have classes explicitly set which
* makes things easier for our Symfony extension to convert things into
* Symfony services.
*
* @param string $serviceType
* @param array $awsServiceConfig The translated AWS configuration array
* @param array $defaultSettings
*
* @return array The final array with inheritance processed
*
*/
protected function processInheritance($serviceType, $awsServiceConfig, $defaultSettings = array())
{
foreach ($awsServiceConfig as $name => &$service) {
$parentName = (!empty($service['extends'])) ? $service['extends'] : null;

if (empty($service['params'])) {
$service['params'] = array();
}

while($parentName != null) {
if (!isset($awsServiceConfig[$parentName])) {
break;
}
$parent = $awsServiceConfig[$parentName];

if (empty($parent['params'])) {
$parent['params'] = array();
}

$service['params'] = array_merge($parent['params'], $service['params']);

if (!isset($service['class']) && isset($parent['class'])) {
$service['class'] = $parent['class'];
}

$parentParentName = (!empty($parent['extends'])) ? $parent['extends'] : null;
if ($parentParentName == $parentName) {
break;
}
$parentName = $parentParentName;
}

$service['params'] = array_merge($defaultSettings, $service['params']);

if (empty($service['class'])) {
// Default to the service type class
$service['class'] = $this->getDefaultAwsClassByServiceType($serviceType);
}
}

return $awsServiceConfig;
}
}
57 changes: 9 additions & 48 deletions DependencyInjection/JLMAwsExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
use Symfony\Component\Config\FileLocator;

use JLM\AwsBundle\Config\AwsConfigTranslator;
use Aws\Common\Aws;
use JLM\AwsBundle\Aws\Common\Aws;
use Symfony\Component\Yaml\Dumper;

/**
Expand All @@ -20,7 +20,8 @@
*/
class JLMAwsExtension extends Extension
{
const AWS_SERVICE_PREFIX = 'jlm_aws.';
const AWS_SERVICE_PREFIX = 'jlm_aws.'; // TODO: Make configurable
const BASE_CLASS = 'JLM\AwsBundle\Aws\Common\Aws'; // TODO: Make configurable

public function getAlias()
{
Expand Down Expand Up @@ -64,68 +65,28 @@ public function load(array $configs, ContainerBuilder $container)
$loader->load($tmpFile);
}

private function getServiceClassFromAwsConfig($service, array $awsConfig, AwsConfigTranslator $awsConfigTranslator)
{
$serviceConfig = $awsConfig['services'];

// simplest case, class is defined
if (isset($serviceConfig['class'])) {
return $serviceConfig['class'];
}

// if extends is set, we have to walk the chiain until
// a. class is defined for one of the parents
// b. we run out of parents or the class extends itself (exception territory)
if (isset($serviceConfig['extends'])) {
$curExtends = $serviceConfg['extends'];
$curService = $service;
while(true) {
if(!isset($awsConfig['services'][$curExtends])
|| $curExtends == $curService) { // prevent infinite recursion
return $awsConfigTranslator->getDefaultAwsClassByServiceName($curService);
}

$parentConfig = $awsConfig['services'][$curExtends];
if (isset($parentConfig['class'])) {
return $parentConfig['class'];
}

if (isset($parentConfig['extends'])) {
$curService = $curExtends;
$curExtends = $parentConfig['extends'];
} else {
return $awsConfigTranslator->getDefaultAwsClassByServiceName($curService);
}
}
} else {
return $awsConfigTranslator->getDefaultAwsClassByServiceName($service);
}
}

private function generateServicesConfigArray(array $awsConfig, AwsConfigTranslator $awsConfigTranslator)
{
$awsServices['services'] = array(
self::AWS_SERVICE_PREFIX . 'aws' => array(
'class' => 'Aws\Common\Aws',
'factory_class' => 'Aws\Common\Aws',
'class' => self::BASE_CLASS,
'factory_class' => self::BASE_CLASS,
'factory_method' => 'factory',
'arguments' => array($awsConfig)
)
);

foreach ($awsConfig['services'] as $service => $serviceConfig) {
if ($service == 'default_settings') {
continue;
if (!isset($serviceConfig['class'])) {
continue; // Skip any configurations lacking a class as they are abstract
}

$class = $this->getServiceClassFromAwsConfig($service, $awsConfig, $awsConfigTranslator);

$awsServices['services'][self::AWS_SERVICE_PREFIX . $service] = array(
'class' => $class,
'class' => $serviceConfig['class'],
'factory_service' => self::AWS_SERVICE_PREFIX . 'aws',
'factory_method' => 'get',
'arguments' => array($service)
);
);
}

return $awsServices;
Expand Down
Loading

0 comments on commit 85cfd2a

Please sign in to comment.