Skip to content

Commit 74c196c

Browse files
authored
Merge pull request #4 from Setono/query-parameter-matcher
Refactor query parameter matching
2 parents dc168d5 + f63f6af commit 74c196c

File tree

8 files changed

+131
-88
lines changed

8 files changed

+131
-88
lines changed

src/DependencyInjection/Configuration.php

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,27 @@ public function getConfigTreeBuilder(): TreeBuilder
2727
->info('If you want to enable the injection of javascript (through the tag bag bundle), set this to true. Default is true.')
2828
->canBeDisabled()
2929
->end()
30-
->arrayNode('source_parameters')
31-
->info('The query parameters to look for when trying to find the source. Notice that utm_source is matched automatically.')
32-
->defaultValue(['ref', 'source'])
33-
->scalarPrototype()->end()
30+
->arrayNode('query_parameters')
31+
->info('The query parameters to look for when trying to resolve source from query parameters')
32+
->arrayPrototype()
33+
->canBeDisabled()
34+
->children()
35+
->arrayNode('matches')
36+
->beforeNormalization()
37+
->ifString()
38+
->then(function (string $v): array { return [$v]; })
39+
->end()
40+
->scalarPrototype()->end()
41+
->end()
42+
->scalarNode('source')
43+
->info('If not set, the value of the matched query parameter will be used')
44+
->defaultNull()
45+
->cannotBeEmpty()
46+
->end()
47+
->scalarNode('medium')->defaultNull()->cannotBeEmpty()->end()
48+
->scalarNode('campaign')->defaultNull()->cannotBeEmpty()->end()
49+
->end()
50+
->end()
3451
->end()
3552
->integerNode('session_timeout')
3653
->info('The number of seconds you consider a session lifetime. Default is 1800 seconds (30 minutes) which is used by Google Analytics and other analytics tools')

src/DependencyInjection/SetonoSyliusConversionAttributionExtension.php

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,58 @@ public function load(array $configs, ContainerBuilder $container): void
1919
/**
2020
* @psalm-suppress PossiblyNullArgument
2121
*
22-
* @var array{javascript: array{enabled: bool}, source_parameters: list<string>, session_timeout: int, resources: array} $config
22+
* @var array{
23+
* javascript: array{enabled: bool},
24+
* query_parameters: array,
25+
* session_timeout: int,
26+
* resources: array
27+
* } $config
2328
*/
2429
$config = $this->processConfiguration($this->getConfiguration([], $container), $configs);
2530
$loader = new XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
2631

32+
$queryParameters = array_merge_recursive([
33+
'facebook' => [
34+
'matches' => ['fbclid'], 'source' => 'facebook.com', 'medium' => 'cpc',
35+
],
36+
'google' => [
37+
'matches' => ['gclid', 'gbraid', 'wbraid'], 'source' => 'google', 'medium' => 'cpc',
38+
],
39+
'microsoft' => [
40+
'matches' => ['msclkid'], 'source' => 'bing', 'medium' => 'cpc',
41+
],
42+
'generic' => [
43+
'matches' => ['source', 'ref'],
44+
],
45+
'tiktok' => [
46+
'matches' => ['ttclid'], 'source' => 'tiktok', 'medium' => 'cpc',
47+
],
48+
'x' => [
49+
'matches' => ['twclid'], 'source' => 'x', 'medium' => 'cpc',
50+
],
51+
], $config['query_parameters']);
52+
$queryParameters = array_filter($queryParameters, static function (array $queryParameter): bool {
53+
return !array_key_exists('enabled', $queryParameter) || true === $queryParameter['enabled'];
54+
});
55+
$queryParameters = array_map(
56+
/** @param array{matches: list<string>, source?: list<string|null>|string|null, medium?: list<string|null>|string|null, campaign?: list<string|null>|string|null, enabled?: bool} $queryParameter */
57+
static function (array $queryParameter): array {
58+
unset($queryParameter['enabled']);
59+
60+
$queryParameter['matches'] = array_values(array_unique($queryParameter['matches']));
61+
62+
// Because array_merge_recursive will merge associative keys by creating a new array, we will turn it back into a string
63+
$queryParameter['source'] = self::firstValue($queryParameter['source'] ?? null);
64+
$queryParameter['medium'] = self::firstValue($queryParameter['medium'] ?? null);
65+
$queryParameter['campaign'] = self::firstValue($queryParameter['campaign'] ?? null);
66+
67+
return $queryParameter;
68+
},
69+
$queryParameters,
70+
);
71+
2772
$container->setParameter('setono_sylius_conversion_attribution.javascript.enabled', $config['javascript']['enabled']);
28-
$container->setParameter('setono_sylius_conversion_attribution.source_parameters', $config['source_parameters']);
73+
$container->setParameter('setono_sylius_conversion_attribution.query_parameters', $queryParameters);
2974
$container->setParameter('setono_sylius_conversion_attribution.session_timeout', $config['session_timeout']);
3075
$container->setParameter('setono_sylius_conversion_attribution.referrers.cache.file', '%kernel.cache_dir%/referrers.php');
3176

@@ -68,4 +113,19 @@ private static function bundleEnabled(ContainerBuilder $container, string $bundl
68113

69114
return isset($bundles[$bundle->getName()]);
70115
}
116+
117+
/**
118+
* @param string|list<string|null>|null $value
119+
*/
120+
private static function firstValue(null|string|array $value): ?string
121+
{
122+
$array = (array) $value;
123+
foreach ($array as $item) {
124+
if (null !== $item) {
125+
return $item;
126+
}
127+
}
128+
129+
return null;
130+
}
71131
}

src/Matcher/GoogleAdsSourceMatcher.php

Lines changed: 0 additions & 27 deletions
This file was deleted.

src/Matcher/QueryParameterBasedSourceMatcher.php

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,21 @@
99
final class QueryParameterBasedSourceMatcher implements SourceMatcherInterface
1010
{
1111
public function __construct(
12-
/**
13-
* @var list<string> $parameters
14-
*/
15-
public readonly array $parameters,
12+
/** @var list<array{matches: list<string>, source: string|null, medium: string|null, campaign: string|null}> $parameters */
13+
private readonly array $parameters,
1614
) {
1715
}
1816

1917
public function match(Request $request): ?Source
2018
{
2119
foreach ($this->parameters as $parameter) {
22-
$source = $request->query->get($parameter);
23-
if (is_string($source) && '' !== $source) {
24-
return new Source($source);
20+
foreach ($parameter['matches'] as $match) {
21+
$source = $request->query->get($match);
22+
if (!is_string($source) || '' === $source) {
23+
continue;
24+
}
25+
26+
return new Source($parameter['source'] ?? $source, $parameter['medium'], $parameter['campaign']);
2527
}
2628
}
2729

src/Resources/config/services/matcher.xml

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,13 @@
77

88
<service id="Setono\SyliusConversionAttributionPlugin\Matcher\CompositeSourceMatcher"/>
99

10-
<service id="Setono\SyliusConversionAttributionPlugin\Matcher\GoogleAdsSourceMatcher">
11-
<tag name="setono_sylius_conversion_attribution.source_matcher" priority="400"/>
12-
</service>
10+
<service id="Setono\SyliusConversionAttributionPlugin\Matcher\QueryParameterBasedSourceMatcher">
11+
<argument>%setono_sylius_conversion_attribution.query_parameters%</argument>
1312

14-
<service id="Setono\SyliusConversionAttributionPlugin\Matcher\UtmQueryParameterBasedSourceMatcher">
1513
<tag name="setono_sylius_conversion_attribution.source_matcher" priority="300"/>
1614
</service>
1715

18-
<service id="Setono\SyliusConversionAttributionPlugin\Matcher\QueryParameterBasedSourceMatcher">
19-
<argument>%setono_sylius_conversion_attribution.source_parameters%</argument>
20-
16+
<service id="Setono\SyliusConversionAttributionPlugin\Matcher\UtmQueryParameterBasedSourceMatcher">
2117
<tag name="setono_sylius_conversion_attribution.source_matcher" priority="200"/>
2218
</service>
2319

tests/DependencyInjection/SetonoSyliusConversionAttributionExtensionTest.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,29 @@ protected function getContainerExtensions(): array
1717
];
1818
}
1919

20+
/**
21+
* @test
22+
*/
23+
public function it_sets_cpc_parameters(): void
24+
{
25+
$this->load([
26+
'javascript' => false,
27+
'query_parameters' => [
28+
'facebook' => ['enabled' => false], // should exclude facebook from the resolved list
29+
'generic' => ['matches' => ['ref']], // 'ref' should only occur once on the resolved list
30+
'x' => ['matches' => ['xclid']], // should add 'xclid' to the list
31+
],
32+
]);
33+
34+
$this->assertContainerBuilderHasParameter('setono_sylius_conversion_attribution.query_parameters', [
35+
'google' => ['matches' => ['gclid', 'gbraid', 'wbraid'], 'source' => 'google', 'medium' => 'cpc', 'campaign' => null],
36+
'microsoft' => ['matches' => ['msclkid'], 'source' => 'bing', 'medium' => 'cpc', 'campaign' => null],
37+
'generic' => ['matches' => ['source', 'ref'], 'source' => null, 'medium' => null, 'campaign' => null],
38+
'tiktok' => ['matches' => ['ttclid'], 'source' => 'tiktok', 'medium' => 'cpc', 'campaign' => null],
39+
'x' => ['matches' => ['twclid', 'xclid'], 'source' => 'x', 'medium' => 'cpc', 'campaign' => null],
40+
]);
41+
}
42+
2043
/**
2144
* @test
2245
*/

tests/Matcher/GoogleAdsSourceMatcherTest.php

Lines changed: 0 additions & 25 deletions
This file was deleted.

tests/Matcher/QueryParameterBasedSourceMatcherTest.php

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,35 +13,32 @@ final class QueryParameterBasedSourceMatcherTest extends TestCase
1313
/**
1414
* @test
1515
*/
16-
public function it_matches(): void
16+
public function it_matches1(): void
1717
{
1818
$matcher = new QueryParameterBasedSourceMatcher([
19-
'ref',
19+
['matches' => ['gclid'], 'source' => 'google', 'medium' => 'cpc', 'campaign' => 'Summer Sale'],
2020
]);
21+
$res = $matcher->match(new Request(['gclid' => '1234']));
2122

22-
$request = new Request([
23-
'ref' => 'test',
24-
]);
25-
26-
$res = $matcher->match($request);
2723
self::assertNotNull($res);
28-
self::assertSame('test', $res->source);
24+
self::assertSame('google', $res->source);
25+
self::assertSame('cpc', $res->medium);
26+
self::assertSame('Summer Sale', $res->campaign);
2927
}
3028

3129
/**
3230
* @test
3331
*/
34-
public function it_does_not_match(): void
32+
public function it_matches2(): void
3533
{
3634
$matcher = new QueryParameterBasedSourceMatcher([
37-
'ref',
35+
['matches' => ['source'], 'source' => null, 'medium' => null, 'campaign' => null],
3836
]);
37+
$res = $matcher->match(new Request(['source' => 'slashdot.org']));
3938

40-
$request = new Request([
41-
'source' => 'test',
42-
]);
43-
44-
$res = $matcher->match($request);
45-
self::assertNull($res);
39+
self::assertNotNull($res);
40+
self::assertSame('slashdot.org', $res->source);
41+
self::assertNull($res->medium);
42+
self::assertNull($res->campaign);
4643
}
4744
}

0 commit comments

Comments
 (0)