Skip to content
This repository has been archived by the owner on Feb 6, 2020. It is now read-only.

Feature: ReflectionBasedAbstractFactory #153

Merged

Conversation

weierophinney
Copy link
Member

Ports zend-mvc LazyControllerAbstractFactory to zend-servicemanager

This patch ports zend-mvc's LazyControllerAbstractFactory to a more generic ReflectionBasedAbstractFactory that can be used anywhere. The principal change is that it does not restrict type generation to only dispatchables.

One additional change was made: the zend-mvc version hard-codes in a number of "well known" services that use short names, mapping them to the actual classes that handle them. This patch keeps them, but allows you to override them via the constructor.

Documentation is provided, which contrasts it to the ConfigAbstractFactory; benchmarks verify that one principal difference is performance (the ConfigAbstractFactory is 2x faster, but requires more configuration).

I think between this and the ConfigAbstractFactory, we'll have a nice story to tell around developer experience for the 3.2 release.

@weierophinney weierophinney added this to the 3.2.0 milestone Sep 5, 2016
@weierophinney
Copy link
Member Author

Ping @GeeH — I think you may be interested in this. Also pinging @zendframework/community-review-team for review.

@akrabat
Copy link
Contributor

akrabat commented Sep 5, 2016

It is so nice to see documentation with PRs again…

@mtymek
Copy link

mtymek commented Sep 5, 2016

Great to see this in service-manager! 80 to 90% of all factories I write simply pull dependent objects from SM and inject them using constructor. However, what about the caching? Not sure how it is in PHP7, but I remember that Reflection API was considered to be very slow.

I actually come up with the same idea earlier: click. May way of caching is pretty dumb (making it more elegant would require changes in SM core), but it works fine - I will start using it in production in coming month.

Also, I don't like the fact that it comes with code specific to zend-mvc classes. IMO this should be standalone component, not tied to externals. I would rather see a class in zend-mvc extending this one.

@weierophinney
Copy link
Member Author

@mtymek That's a good point. In essence, the zend-mvc LazyControllerAbstractFactory could extend this one with those services added, and a verification in canCreate() to ensure a dispatchable is present, and we could ship another version in the Zend\Mvc\Service namespace with these default services.

I'll update the PR momentarily to remove the initial definitions, as well as to document that aspect of both instantiation and extension. Thanks for the feedback!

@weierophinney
Copy link
Member Author

@mtymek In terms of caching, that's an idea I discussed with @GeeH with regards to the ConfigAbstractFactory as well; essentially, it'd be a script that you'd invoke, and it would create the factor(y|ies) for you, based on either (a) configuration (in the example of ConfigAbstractFactory), and/or (b) provided class name argument (either abstract factory type). Ideally, this could be a part of a new zftool iteration. I don't think it needs to be part of this specific iteration, however.

@weierophinney
Copy link
Member Author

It occurs to me that part of the difference in performance may be due to how the benchmark is structured; it has to go through two other abstract factories before this one is hit. I'll see if I can find an improved way to test performance, though I'm not necessarily sure it it needs to be part of this PR.

@GeeH
Copy link
Contributor

GeeH commented Sep 6, 2016

I'm not convinced this is of value in production without some way to cache the reflection so it's not performed in production. This is not sour grapes because you are diluting the praise I received for the ConfigAbstractFactory, oh no, no siree. It's just that I worry that this will become the "norm" and people will start to moan about how bad performance of ZF is (again). I definitely think this is worth exploring however.

@weierophinney
Copy link
Member Author

weierophinney commented Sep 6, 2016

I'm making this as a WIP, with the following goals:

  • more benchmarking, including isolated benchmarks, usage as a mapped factory, and in s chain with the config-based factory.
  • creation of a tool to create a factory for a named class.
  • a separate PR creating a tool that will create factories for configured config-based entries, as well as for creating the original entries.

@GeeH is correct - DX is great, but we need an end-to-end solution that also provides production performance.

@GeeH
Copy link
Contributor

GeeH commented Sep 6, 2016

@weierophinney FWIW I am working on a CLI tool right now for ConfigAbstractFactory, can you ping me on IRC when you have a minute to discuss how I can do both.

@weierophinney
Copy link
Member Author

Benchmarks:

benchmark subject group params revs its mem best mean mode worst stdev rstdev diff
SetNewServicesBench benchSetFactory [] 1000 10 943,328b 2.727μs 2.800μs 2.790μs 2.929μs 0.055μs 1.97% +873.81%
SetNewServicesBench benchSetAlias [] 1000 10 943,328b 6.980μs 7.277μs 7.314μs 7.468μs 0.140μs 1.93% +2,431.10%
SetNewServicesBench benchSetAliasOverrided [] 1000 10 943,336b 21.413μs 22.047μs 21.952μs 22.935μs 0.422μs 1.91% +7,568.38%
FetchNewServiceUsingConfigAbstractFactoryAsFactoryBench benchFetchServiceWithNoDependencies [] 1000 10 914,216b 3.263μs 3.320μs 3.303μs 3.378μs 0.037μs 1.10% +1,054.61%
FetchNewServiceUsingConfigAbstractFactoryAsFactoryBench benchBuildServiceWithNoDependencies [] 1000 10 914,216b 2.821μs 2.913μs 2.893μs 3.013μs 0.059μs 2.02% +913.11%
FetchNewServiceUsingConfigAbstractFactoryAsFactoryBench benchFetchServiceDependingOnConfig [] 1000 10 914,216b 3.977μs 4.062μs 4.041μs 4.172μs 0.057μs 1.40% +1,312.97%
FetchNewServiceUsingConfigAbstractFactoryAsFactoryBench benchBuildServiceDependingOnConfig [] 1000 10 914,216b 3.527μs 3.683μs 3.601μs 3.857μs 0.116μs 3.16% +1,180.90%
FetchNewServiceUsingConfigAbstractFactoryAsFactoryBench benchFetchServiceWithDependency [] 1000 10 914,208b 3.860μs 4.006μs 3.981μs 4.134μs 0.084μs 2.11% +1,293.53%
FetchNewServiceUsingConfigAbstractFactoryAsFactoryBench benchBuildServiceWithDependency [] 1000 10 914,208b 3.550μs 3.647μs 3.630μs 3.762μs 0.063μs 1.74% +1,168.59%
FetchNewServiceUsingReflectionAbstractFactoryAsFactoryBench benchFetchServiceWithNoDependencies [] 1000 10 912,344b 2.714μs 2.799μs 2.805μs 2.913μs 0.055μs 1.98% +873.43%
FetchNewServiceUsingReflectionAbstractFactoryAsFactoryBench benchBuildServiceWithNoDependencies [] 1000 10 912,344b 2.390μs 2.446μs 2.456μs 2.537μs 0.044μs 1.80% +750.82%
FetchNewServiceUsingReflectionAbstractFactoryAsFactoryBench benchFetchServiceDependingOnConfig [] 1000 10 912,344b 5.384μs 5.497μs 5.443μs 5.710μs 0.100μs 1.81% +1,812.03%
FetchNewServiceUsingReflectionAbstractFactoryAsFactoryBench benchBuildServiceDependingOnConfig [] 1000 10 912,344b 4.926μs 5.125μs 5.105μs 5.313μs 0.118μs 2.30% +1,682.75%
FetchNewServiceUsingReflectionAbstractFactoryAsFactoryBench benchFetchServiceWithDependency [] 1000 10 912,336b 6.263μs 6.403μs 6.410μs 6.522μs 0.072μs 1.12% +2,127.17%
FetchNewServiceUsingReflectionAbstractFactoryAsFactoryBench benchBuildServiceWithDependency [] 1000 10 912,336b 5.771μs 5.981μs 5.901μs 6.261μs 0.159μs 2.66% +1,980.42%
FetchNewServiceManagerBench benchFetchServiceManagerCreation [] 100 20 24,043,720b 2,537.990μs 2,656.568μs 2,624.420μs 2,775.440μs 74.290μs 2.80% +923,923.65%
FetchCachedServicesBench benchFetchFactory1 [] 1000 20 914,520b 0.284μs 0.294μs 0.290μs 0.306μs 0.006μs 2.11% +2.14%
FetchCachedServicesBench benchFetchInvokable1 [] 1000 20 914,520b 0.292μs 0.300μs 0.300μs 0.310μs 0.006μs 1.88% +4.35%
FetchCachedServicesBench benchFetchService1 [] 1000 20 914,520b 0.281μs 0.289μs 0.283μs 0.302μs 0.008μs 2.61% +0.61%
FetchCachedServicesBench benchFetchAlias1 [] 1000 20 914,520b 0.281μs 0.288μs 0.283μs 0.299μs 0.005μs 1.90% 0.00%
FetchCachedServicesBench benchFetchRecursiveAlias1 [] 1000 20 914,528b 0.292μs 0.299μs 0.294μs 0.314μs 0.006μs 2.12% +4.05%
FetchCachedServicesBench benchFetchRecursiveAlias2 [] 1000 20 914,528b 0.292μs 0.297μs 0.293μs 0.310μs 0.006μs 1.87% +3.23%
FetchCachedServicesBench benchFetchAbstractFactoryService [] 1000 20 914,536b 1.611μs 1.655μs 1.641μs 1.711μs 0.030μs 1.84% +475.57%
FetchNewServiceViaConfigAbstractFactoryBench benchFetchServiceWithNoDependencies [] 1000 10 913,808b 3.542μs 3.650μs 3.648μs 3.748μs 0.054μs 1.49% +1,169.39%
FetchNewServiceViaConfigAbstractFactoryBench benchBuildServiceWithNoDependencies [] 1000 10 913,808b 3.129μs 3.254μs 3.297μs 3.363μs 0.079μs 2.41% +1,031.97%
FetchNewServiceViaConfigAbstractFactoryBench benchFetchServiceDependingOnConfig [] 1000 10 913,808b 4.163μs 4.320μs 4.272μs 4.515μs 0.106μs 2.46% +1,402.50%
FetchNewServiceViaConfigAbstractFactoryBench benchBuildServiceDependingOnConfig [] 1000 10 913,808b 3.649μs 3.770μs 3.792μs 3.831μs 0.053μs 1.39% +1,211.30%
FetchNewServiceViaConfigAbstractFactoryBench benchFetchServiceWithDependency [] 1000 10 913,800b 4.020μs 4.186μs 4.190μs 4.356μs 0.092μs 2.19% +1,355.86%
FetchNewServiceViaConfigAbstractFactoryBench benchBuildServiceWithDependency [] 1000 10 913,800b 3.642μs 3.731μs 3.764μs 3.802μs 0.053μs 1.43% +1,197.67%
FetchNewServiceViaReflectionAbstractFactoryBench benchFetchServiceWithNoDependencies [] 1000 10 912,032b 2.295μs 2.368μs 2.359μs 2.470μs 0.046μs 1.96% +723.72%
FetchNewServiceViaReflectionAbstractFactoryBench benchBuildServiceWithNoDependencies [] 1000 10 912,032b 1.895μs 1.949μs 1.930μs 2.033μs 0.044μs 2.28% +577.74%
FetchNewServiceViaReflectionAbstractFactoryBench benchFetchServiceDependingOnConfig [] 1000 10 912,032b 5.013μs 5.145μs 5.089μs 5.328μs 0.103μs 2.00% +1,689.67%
FetchNewServiceViaReflectionAbstractFactoryBench benchBuildServiceDependingOnConfig [] 1000 10 912,032b 4.492μs 4.650μs 4.703μs 4.806μs 0.100μs 2.14% +1,517.50%
FetchNewServiceViaReflectionAbstractFactoryBench benchFetchServiceWithDependency [] 1000 10 912,024b 5.749μs 6.020μs 6.111μs 6.223μs 0.165μs 2.74% +1,993.84%
FetchNewServiceViaReflectionAbstractFactoryBench benchBuildServiceWithDependency [] 1000 10 912,024b 5.281μs 5.359μs 5.313μs 5.461μs 0.061μs 1.15% +1,764.03%
FetchNewServicesBench benchFetchFactory1 [] 1000 10 916,528b 1.774μs 1.833μs 1.813μs 1.917μs 0.043μs 2.34% +537.53%
FetchNewServicesBench benchBuildFactory1 [] 1000 10 916,528b 1.480μs 1.543μs 1.566μs 1.593μs 0.038μs 2.46% +436.77%
FetchNewServicesBench benchFetchInvokable1 [] 1000 10 916,528b 2.125μs 2.210μs 2.213μs 2.317μs 0.054μs 2.42% +668.77%
FetchNewServicesBench benchBuildInvokable1 [] 1000 10 916,528b 1.662μs 1.741μs 1.735μs 1.814μs 0.045μs 2.57% +505.57%
FetchNewServicesBench benchFetchService1 [] 1000 10 916,528b 0.294μs 0.304μs 0.303μs 0.319μs 0.008μs 2.54% +5.84%
FetchNewServicesBench benchFetchFactoryAlias1 [] 1000 10 916,528b 1.506μs 1.576μs 1.578μs 1.650μs 0.040μs 2.55% +448.31%
FetchNewServicesBench benchBuildFactoryAlias1 [] 1000 10 916,528b 1.497μs 1.555μs 1.565μs 1.612μs 0.034μs 2.19% +440.87%
FetchNewServicesBench benchFetchRecursiveFactoryAlias1 [] 1000 10 916,544b 1.577μs 1.643μs 1.644μs 1.703μs 0.030μs 1.83% +471.51%
FetchNewServicesBench benchBuildRecursiveFactoryAlias1 [] 1000 10 916,544b 1.608μs 1.629μs 1.622μs 1.687μs 0.021μs 1.30% +466.43%
FetchNewServicesBench benchFetchRecursiveFactoryAlias2 [] 1000 10 916,544b 1.573μs 1.610μs 1.598μs 1.651μs 0.026μs 1.60% +460.10%
FetchNewServicesBench benchBuildRecursiveFactoryAlias2 [] 1000 10 916,544b 1.594μs 1.669μs 1.652μs 1.745μs 0.050μs 2.97% +480.45%
FetchNewServicesBench benchFetchAbstractFactoryFoo [] 1000 10 916,536b 1.558μs 1.593μs 1.578μs 1.657μs 0.032μs 2.03% +454.12%
FetchNewServicesBench benchBuildAbstractFactoryFoo [] 1000 10 916,536b 1.216μs 1.271μs 1.279μs 1.305μs 0.027μs 2.16% +342.09%

@weierophinney weierophinney removed the WIP label Sep 14, 2016
@weierophinney
Copy link
Member Author

Summary of the benchmarks:

  • The ConfigAbstractFactory runs at around the same speed as any other factory that pulls at least one dependency from the container.
  • The ReflectionBasedAbstractFactory adds 2-3μs to service creation for a service that pulls at least one dependency from the container; this is essentially the cost of using reflection on the constructor.
  • The ConfigAbstractFactory runs essentially the same speed whether used as an abstract factory or mapped as a factory; these speeds will, of course, change based on how many abstract factories are in use.
  • The ReflectionBasedAbstractFactory consistently runs 0.5μs faster when mapped as a factory, regardless of number of dependencies.

The takeaways are:

  • The ReflectionBasedAbstractFactory is more convenient (less configuration required), but comes at a performance cost.
  • The ConfigAbstractFactory is faster, but comes with more configuration; it also has the benefit that you can specify alternate services (e.g., mapping config sub-arrays to a configuration abstract factory).
  • A factory that does not do reflection, and does not need to first pull the config service in order to determine dependencies is still the fastest solution.

#154 solves for the performance aspect, as well as the configuration aspect of the ConfigAbstractFactory.

My take is that an application will evolve over time:

  • The developer can use the ReflectionBasedAbstractFactory as a catch-all to begin application development.
  • They can then generate configuration for the ConfigAbstractFactory for a performance boost, to specify alternate dependencies, or to disambiguate dependencies.
  • Finally, when ready to go to production, they can generate actual factory classes, and map them into configuration.

Ready for review, @zendframework/community-review-team !

return function (ReflectionParameter $parameter) use ($container, $requestedName, $hasConfig) {
if ($parameter->isArray()
&& $parameter->getName() === 'config'
&& $hasConfig

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about branching on $hasConfig outside this closure and return a specialized closure for each case?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll give it a try and see if it makes any difference on performance.

}

if ($parameter->isArray()) {
return [];
Copy link

@danizord danizord Sep 15, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the point of passing an empty array here? I'd expect it to throw an exception if the parameter could not be resolved.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since it's intended as a rapid application development tool, the idea here is to not error for an array, as we can supply one, even if it's empty. The class itself can raise an exception if the empty array will not work.


if (! $container->has($type)) {
throw new ServiceNotFoundException(sprintf(
'Unable to create controller "%s"; unable to resolve parameter "%s" using type hint "%s"',

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/controller/instance of

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch; remnant from the LazyControllerAbstractFactory; will update shortly.

}

if (! $parameter->getClass()) {
return;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here I'd expect it to throw an exception as well instead of failing silently.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good point; I'll make that change shortly.

This patch ports zend-mvc's `LazyControllerAbstractFactory` to a more
generic `ReflectionBasedAbstractFactory` that can be used anywhere. The
principal change is that it does not restrict type generation to only
dispatchables.

One additional change was made: the zend-mvc version hard-codes in a
number of "well known" services that use short names, mapping them to
the actual classes that handle them. This patch keeps them, but allows
you to override them via the constructor.
Includes a comparison with the `ConfigAbstractFactory`.
Looks like it is 100% slower than the ConfigAbstractFactory, which verifies the
assumptions made in the documentation.
Ensures de-coupling, and allows the user to configure which services may
be necessary to map.
This patch provides more benchmarking for the new abstract factories, pulling
them each into separate benchmark classes to ensure they operate in isolation to
other abstract factories, and to allow seeing baseline timings.

Additionally, each has two benchmarking suites: one demonstrating usage as an
abstract factory, another demonstrating usage as a mapped factory.

The basic results are that the `ConfigAbstractFactory` operates at roughly the
same speed as a normal abstract factory that contains logic for creating an
instance. The `ReflectionBasedAbstractFactory` is about 100% slower when at
least one dependency is present (and ever-so-slightly faster when no
dependencies are present).
- Split the parameter resolution into three:
  - A method that handles resolving arrays, detecting typehints, etc. common
    across all invocations.
  - A method that delegates to the above when the `config` service is not
    present.
  - A method that will check if the provided parameter name is `config` and hits
    against `array`, returning the `config` service; if not, it delegates to the
    first.
  - The main logic now has a ternary condition to determine which of the latter
    two to use, based on whether or not the `config` service is present.
- Raise an exception when detecting a scalar argument (instead of passing null).
- `s/controller/service/` in exception messages.
@weierophinney
Copy link
Member Author

@danizord Incorporated your feedback; thanks!

if (! $parameter->getClass()) {
return;
}
if (! $parameter->getClass()) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If $parameter->isDefaultValueAvailable(), we could return $parameter->getDefaultValue() here before falling to exception.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay; will push that momentarily.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

@weierophinney
Copy link
Member Author

After making the changes suggested by @danizord , the benchmarks improved; the ReflectionBasedAbstractFactory is now only around 50% slower than the ConfigAbstractFactory, and, in the case of no constructor arguments, around 50% faster.

private function resolveParameter(ReflectionParameter $parameter, ContainerInterface $container, $requestedName)
{
if ($parameter->isArray()) {
return [];

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could use default value if available here as well?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Default can only be an empty array or null; not sure the overhead of retrieving the value via reflection is worth it.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@weierophinney If a parameter has a default value as null it throws an exception.
I just bumped into it using Zend Expressive Skeleton with default configuration I get Unable to create service "Zend\Expressive\Middleware\ImplicitHeadMiddleware"; unable to resolve parameter "response" using type hint "Psr\Http\Message\ResponseInterface".
As solution can be change this:

if (! $container->has($type)) {
	throw new ServiceNotFoundException(sprintf(
		'Unable to create service "%s"; unable to resolve parameter "%s" using type hint "%s"',
		$requestedName,
		$parameter->getName(),
		$type
	));
}

return $container->get($type);

to

if ($container->has($type)) {
	return $container->get($type);
} elseif ($parameter->isOptional()) {
	return $parameter->getDefaultValue();
}

throw new ServiceNotFoundException(sprintf(
	'Unable to create service "%s"; unable to resolve parameter "%s" using type hint "%s"',
	$requestedName,
	$parameter->getName(),
	$type
));

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@popovserhii
Please open a new issue report for this problem. Thanks!

Copy link
Member

@DASPRiD DASPRiD left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a minor optimization tweak. Apart from that, this looks good to me!

*/
public function __construct(array $aliases = [])
{
if (! empty($aliases)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it actually worth doing the empty() check, instead of just assigning directly? (In that case, there'd be no need for the direct property initialization.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An extending class may define defaults, in which case casting to an empty array would be problematic.

Copy link
Member

@DASPRiD DASPRiD Sep 15, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case though, shouldn't the aliases get appended, not override when not empty?

$this->aliases += $aliases;

Although in that case again, the not empty checked is not needed.

@weierophinney weierophinney merged commit 42176f4 into zendframework:develop Sep 15, 2016
weierophinney added a commit that referenced this pull request Sep 15, 2016
weierophinney added a commit that referenced this pull request Sep 15, 2016
@weierophinney weierophinney deleted the feature/reflection-factory branch September 15, 2016 20:44
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants