Skip to content

Commit 5aef30f

Browse files
committed
Upload customer lists to Google Ads
1 parent 93269c6 commit 5aef30f

File tree

94 files changed

+3001
-46
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

94 files changed

+3001
-46
lines changed

CLAUDE.md

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
This is a Sylius plugin that integrates Google Ads conversion tracking with Sylius e-commerce stores. The plugin uses the Google Ads API directly (server-side) instead of JavaScript tracking, providing better control over consent, conversion values, and avoiding ad blockers.
8+
9+
## Development Commands
10+
11+
### Testing
12+
```bash
13+
# Run all tests
14+
./vendor/bin/phpunit
15+
16+
# Run specific test
17+
./vendor/bin/phpunit tests/Resolver/CustomerIdsResolverTest.php
18+
```
19+
20+
### Code Quality
21+
```bash
22+
# Code style checking
23+
composer check-style
24+
25+
# Fix code style issues
26+
composer fix-style
27+
28+
# Static analysis
29+
composer analyse
30+
```
31+
32+
### Plugin Console Commands
33+
```bash
34+
# Process Google Ads conversions (run regularly, e.g., every 5 minutes)
35+
php bin/console setono:sylius-google-ads:process-conversions
36+
37+
# Prune old conversions from database (run daily)
38+
php bin/console setono:sylius-google-ads:prune-conversions
39+
```
40+
41+
## Architecture
42+
43+
### Core Components
44+
45+
**Models**: Connection, Conversion, CustomerList, ConnectionMapping, MerchantMapping
46+
- Connection: Stores Google Ads API credentials and configuration
47+
- Conversion: Represents tracked conversions with state workflow
48+
- CustomerList: Manages Google Ads customer lists for remarketing
49+
50+
**Processing Flow**:
51+
1. **Event Capture**: PurchaseListener captures order events
52+
2. **Conversion Creation**: ConversionFactory creates conversion records
53+
3. **Qualification**: QualificationVoters determine if conversion should be processed
54+
4. **Processing**: ConversionProcessor handles API calls to Google Ads
55+
5. **State Management**: ConversionWorkflow manages conversion states
56+
57+
**Message System**: Uses Symfony Messenger for async processing
58+
- ProcessConversion: Handles individual conversion processing
59+
- ProcessCustomerList/UploadCustomerList: Manages customer list operations
60+
61+
### Key Directories
62+
63+
- `src/Model/`: Core domain models and interfaces
64+
- `src/ConversionProcessor/`: Conversion processing logic with qualification voters
65+
- `src/Message/`: Symfony Messenger commands and handlers
66+
- `src/Client/`: Google Ads API client wrapper and resources
67+
- `src/EventSubscriber/`: Event-driven processing subscribers
68+
- `src/Factory/`: Object creation factories
69+
- `src/Repository/`: Data access layer
70+
- `src/Resources/config/`: Service definitions and configuration
71+
72+
### Google Ads Integration
73+
74+
The plugin integrates with Google Ads through:
75+
- **OAuth2 Flow**: Managed via Controller/Action classes for setup
76+
- **API Client**: Wraps Google Ads PHP library with custom resource classes
77+
- **Conversion Tracking**: Server-side conversion uploads with user identifiers
78+
- **Customer Lists**: Audience management for remarketing campaigns
79+
80+
### State Management
81+
82+
Conversions follow a workflow with states:
83+
- Created → Processing → Delivered/Failed
84+
- Managed by ConversionWorkflow class
85+
- Supports retry logic for failed conversions
86+
87+
## Configuration
88+
89+
Plugin configuration is in `src/Resources/config/app/config.yaml` and requires:
90+
- Google Ads API credentials (client ID, secret, developer token)
91+
- Customer ID mapping for multi-account setups
92+
- Conversion action mapping for different event types
93+
94+
## Testing
95+
96+
The plugin includes both unit tests and integration tests. Live testing can be enabled by setting `GOOGLE_ADS_LIVE=1` and providing real API credentials in phpunit.xml.
97+
98+
- Use prophecy for mocks
99+
100+
## Important Notes
101+
102+
- Customer Lists feature is marked as experimental
103+
- Requires gRPC PHP extension for optimal performance
104+
- Uses Doctrine ORM for persistence
105+
- Integrates with Sylius workflow and event systems
106+
- Supports multi-channel and multi-customer configurations

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,12 @@ Now the plugin is installed. Please read the next section to learn how to use it
9797

9898
To start using the plugin, go to https://your-domain.com/admin/google-ads and follow the instructions.
9999

100+
# Backward compatibility
101+
102+
The plugin adheres to semantic versioning, however, if a feature is `experimental` it is not included in the promise.
103+
104+
Right now the customer lists feature is experimental.
105+
100106
[ico-version]: https://poser.pugx.org/setono/sylius-google-ads-plugin/v/stable
101107
[ico-license]: https://poser.pugx.org/setono/sylius-google-ads-plugin/license
102108
[ico-github-actions]: https://github.com/Setono/SyliusGoogleAdsPlugin/workflows/build/badge.svg

composer.json

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,25 @@
99
],
1010
"require": {
1111
"php": ">=8.1",
12-
"brick/phonenumber": "^0.5.0",
12+
"brick/phonenumber": "^0.7.0",
1313
"doctrine/collections": "^1.6",
1414
"doctrine/dbal": "^2.13 || ^3.8",
1515
"doctrine/orm": "^2.7",
1616
"doctrine/persistence": "^1.3 || ^2.2 || ^3.2",
17-
"google/auth": "^1.26",
18-
"google/gax": "^1.19",
17+
"google/auth": "^1.44",
18+
"google/gax": "^1.35.1",
1919
"googleads/google-ads-php": "^27.2",
2020
"knplabs/knp-menu": "^3.1",
21-
"ocramius/doctrine-batch-utils": "^2.4",
21+
"ocramius/doctrine-batch-utils": "^2.6",
2222
"psr/event-dispatcher": "^1.0",
2323
"psr/log": "^1.0 || ^2.0 || ^3.0",
24-
"setono/composite-compiler-pass": "^1.1",
25-
"setono/doctrine-orm-trait": "^1.0",
24+
"setono/composite-compiler-pass": "^1.2",
25+
"setono/doctrine-orm-trait": "^1.2",
2626
"sylius/channel": "^1.0",
2727
"sylius/channel-bundle": "^1.0",
2828
"sylius/core": "^1.0",
2929
"sylius/core-bundle": "^1.0",
30+
"sylius/customer": "^1.0",
3031
"sylius/order": "^1.0",
3132
"sylius/resource-bundle": "^1.6",
3233
"sylius/ui-bundle": "^1.0",
@@ -55,6 +56,7 @@
5556
"jms/serializer-bundle": "^4.2",
5657
"lexik/jwt-authentication-bundle": "^2.17",
5758
"matthiasnoback/symfony-config-test": "^4.3 || ^5.1",
59+
"phpspec/prophecy-phpunit": "^2.4",
5860
"setono/client-bundle": "^1.1",
5961
"setono/sylius-plugin-pack": "^1.14",
6062
"sylius-labs/polyfill-symfony-security": "^1.1.2",

psalm.xml

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,18 @@
33
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
44
xmlns="https://getpsalm.org/schema/config"
55
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
6-
phpVersion="8.1"
7-
errorLevel="1"
86
findUnusedBaselineEntry="false"
97
findUnusedPsalmSuppress="false"
108
findUnusedCode="false"
119
findUnusedVariablesAndParams="false"
10+
phpVersion="8.1"
11+
errorLevel="1"
1212
>
1313
<projectFiles>
1414
<directory name="src"/>
1515
<directory name="tests"/>
1616
<ignoreFiles>
17+
<file name="src/Message/Handler/UploadCustomersHandler.php"/>
1718
<directory name="tests/Application"/>
1819
<directory name="vendor"/>
1920
</ignoreFiles>
@@ -24,7 +25,6 @@
2425
</plugins>
2526
<issueHandlers>
2627
<PluginIssue name="QueryBuilderSetParameter" errorLevel="info"/>
27-
<UnnecessaryVarAnnotation errorLevel="suppress"/> <!-- We use unnecessary var annotations to aid the IDE -->
2828
<TooManyTemplateParams>
2929
<errorLevel type="suppress">
3030
<directory name="src/Factory"/> <!-- Sylius added @template annotations in later editions -->
@@ -33,4 +33,12 @@
3333
</TooManyTemplateParams>
3434
<RedundantConditionGivenDocblockType errorLevel="suppress"/>
3535
</issueHandlers>
36+
<forbiddenFunctions>
37+
<function name="dd"/>
38+
<function name="dump"/>
39+
<function name="echo"/>
40+
<function name="exit"/>
41+
<function name="var_dump"/>
42+
<function name="print_r"/>
43+
</forbiddenFunctions>
3644
</psalm>

src/Client/Client.php

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Setono\SyliusGoogleAdsPlugin\Client;
6+
7+
use Google\Ads\GoogleAds\Lib\V19\GoogleAdsClient;
8+
use Google\Ads\GoogleAds\V19\Services\SearchGoogleAdsStreamRequest;
9+
use Setono\SyliusGoogleAdsPlugin\Client\Resource\OfflineUserDataJobsResource;
10+
use Setono\SyliusGoogleAdsPlugin\Client\Resource\OfflineUserDataJobsResourceInterface;
11+
use Setono\SyliusGoogleAdsPlugin\Client\Resource\UserListCustomerTypesResource;
12+
use Setono\SyliusGoogleAdsPlugin\Client\Resource\UserListCustomerTypesResourceInterface;
13+
use Setono\SyliusGoogleAdsPlugin\Client\Resource\UserListsResource;
14+
use Setono\SyliusGoogleAdsPlugin\Client\Resource\UserListsResourceInterface;
15+
16+
final class Client implements ClientInterface
17+
{
18+
private ?OfflineUserDataJobsResourceInterface $offlineUserDataJobsResource = null;
19+
20+
private ?UserListsResourceInterface $userListsResource = null;
21+
22+
private ?UserListCustomerTypesResourceInterface $userListCustomerTypesResource = null;
23+
24+
public function __construct(
25+
private readonly GoogleAdsClient $googleAdsClient,
26+
private readonly string $customerId,
27+
) {
28+
}
29+
30+
public function offlineUserDataJobs(): OfflineUserDataJobsResourceInterface
31+
{
32+
if (null === $this->offlineUserDataJobsResource) {
33+
$this->offlineUserDataJobsResource = new OfflineUserDataJobsResource($this, $this->googleAdsClient);
34+
}
35+
36+
return $this->offlineUserDataJobsResource;
37+
}
38+
39+
public function userLists(): UserListsResourceInterface
40+
{
41+
if (null === $this->userListsResource) {
42+
$this->userListsResource = new UserListsResource($this, $this->googleAdsClient);
43+
}
44+
45+
return $this->userListsResource;
46+
}
47+
48+
public function userListCustomerTypes(): UserListCustomerTypesResourceInterface
49+
{
50+
if (null === $this->userListCustomerTypesResource) {
51+
$this->userListCustomerTypesResource = new UserListCustomerTypesResource($this, $this->googleAdsClient);
52+
}
53+
54+
return $this->userListCustomerTypesResource;
55+
}
56+
57+
public function search(string $query): SearchResponse
58+
{
59+
return SearchResponse::fromServerStream($this->googleAdsClient->getGoogleAdsServiceClient()->searchStream(
60+
SearchGoogleAdsStreamRequest::build($this->customerId, $query),
61+
));
62+
}
63+
64+
public function getCustomerId(): string
65+
{
66+
return $this->customerId;
67+
}
68+
}

src/Client/ClientInterface.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Setono\SyliusGoogleAdsPlugin\Client;
6+
7+
use Setono\SyliusGoogleAdsPlugin\Client\Resource\OfflineUserDataJobsResourceInterface;
8+
use Setono\SyliusGoogleAdsPlugin\Client\Resource\UserListCustomerTypesResourceInterface;
9+
use Setono\SyliusGoogleAdsPlugin\Client\Resource\UserListsResourceInterface;
10+
11+
interface ClientInterface
12+
{
13+
public function userLists(): UserListsResourceInterface;
14+
15+
public function userListCustomerTypes(): UserListCustomerTypesResourceInterface;
16+
17+
public function offlineUserDataJobs(): OfflineUserDataJobsResourceInterface;
18+
19+
public function search(string $query): SearchResponse;
20+
21+
public function getCustomerId(): string;
22+
}

0 commit comments

Comments
 (0)