Skip to content

Commit 0f977f3

Browse files
committed
Add Facade, improve performance and ergonomics
This change introduces Laravel-specific patterns for interacting with WorkOS: * A WorkOS facade for static access (e.g. WorkOS::userManagement() ->listUsers()) * A global workos() helper function for fluent access everywhere (e.g. workos()->userManagement()->listUsers()) * WorkOSService singleton with cached service instances * Dependency injection support via service container binding Additionally, while working on this I noticed a potential performance issue. Previously the service container was setting the API Key and Client ID on every request. The service container now configures the SDK only when requested, and its configuration is cached on subsequent requests.
1 parent f8b6508 commit 0f977f3

File tree

7 files changed

+291
-23
lines changed

7 files changed

+291
-23
lines changed

composer.json

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,25 @@
1919
],
2020
"require": {
2121
"php": ">=8.1.0",
22+
"illuminate/contracts": "^11.0|^12.0",
23+
"illuminate/support": "^11.0|^12.0",
2224
"workos/workos-php": "^v4.29.0"
2325
},
2426
"require-dev": {
2527
"friendsofphp/php-cs-fixer": "^2.15 || ^3.6",
26-
"phpunit/phpunit": "^5.7 || ^10.1"
28+
"phpunit/phpunit": "^5.7 || ^10.1",
29+
"orchestra/testbench": "^9.0|^10.0"
2730
},
2831
"suggest": {
2932
"laravel/framework": "For testing"
3033
},
3134
"autoload": {
3235
"psr-4": {
3336
"WorkOS\\Laravel\\": "lib/"
34-
}
37+
},
38+
"files": [
39+
"lib/helpers.php"
40+
]
3541
},
3642
"autoload-dev": {
3743
"psr-4": {
@@ -45,7 +51,10 @@
4551
"laravel": {
4652
"providers": [
4753
"WorkOS\\Laravel\\WorkOSServiceProvider"
48-
]
54+
],
55+
"aliases": {
56+
"WorkOS": "WorkOS\\Laravel\\Facades\\WorkOS"
57+
}
4958
}
5059
},
5160
"scripts": {

lib/Facades/WorkOS.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace WorkOS\Laravel\Facades;
6+
7+
use Illuminate\Support\Facades\Facade;
8+
9+
/**
10+
* @method static \WorkOS\AuditLogs auditLogs()
11+
* @method static \WorkOS\DirectorySync directorySync()
12+
* @method static \WorkOS\MFA mfa()
13+
* @method static \WorkOS\Organizations organizations()
14+
* @method static \WorkOS\Portal portal()
15+
* @method static \WorkOS\SSO sso()
16+
* @method static \WorkOS\UserManagement userManagement()
17+
*
18+
* @see \WorkOS\Laravel\WorkOSService
19+
*/
20+
class WorkOS extends Facade
21+
{
22+
protected static function getFacadeAccessor(): string
23+
{
24+
return 'workos';
25+
}
26+
}

lib/Services/WorkOSService.php

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace WorkOS\Laravel\Services;
6+
7+
use InvalidArgumentException;
8+
use WorkOS\AuditLogs;
9+
use WorkOS\DirectorySync;
10+
use WorkOS\MFA;
11+
use WorkOS\Organizations;
12+
use WorkOS\Portal;
13+
use WorkOS\SSO;
14+
use WorkOS\UserManagement;
15+
16+
/**
17+
* A singleton class that provides a fluent interface for accessing WorkOS services
18+
* in a Laravel Application.
19+
*
20+
* @method \WorkOS\AuditLogs auditLogs()
21+
* @method \WorkOS\DirectorySync directorySync()
22+
* @method \WorkOS\MFA mfa()
23+
* @method \WorkOS\Organizations organizations()
24+
* @method \WorkOS\Portal portal()
25+
* @method \WorkOS\SSO sso()
26+
* @method \WorkOS\UserManagement userManagement()
27+
*/
28+
class WorkOSService
29+
{
30+
/**
31+
* The array of cached service instances
32+
*
33+
* @var array
34+
*/
35+
private $instances = [];
36+
37+
/**
38+
* Map of supported services to their class names
39+
*
40+
* @var array
41+
*/
42+
private $serviceMap = [
43+
'auditLogs' => AuditLogs::class,
44+
'directorySync' => DirectorySync::class,
45+
'mfa' => MFA::class,
46+
'organizations' => Organizations::class,
47+
'portal' => Portal::class,
48+
'sso' => SSO::class,
49+
'userManagement' => UserManagement::class,
50+
];
51+
52+
/**
53+
* Dynamically resolve a WorkOS service.
54+
*
55+
* @param string $name
56+
* @param array $arguments
57+
* @return mixed
58+
*/
59+
public function __call($name, $arguments)
60+
{
61+
if (! array_key_exists($name, $this->serviceMap)) {
62+
throw new InvalidArgumentException("WorkOS service [$name] is not supported.");
63+
}
64+
65+
if (isset($this->instances[$name])) {
66+
return $this->instances[$name];
67+
}
68+
69+
return $this->instances[$name] = $arguments ? new $this->serviceMap[$name]($arguments) : new $this->serviceMap[$name];
70+
}
71+
}

lib/WorkOSServiceProvider.php

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
<?php
22

3+
declare(strict_types=1);
4+
35
namespace WorkOS\Laravel;
46

57
use Illuminate\Support\ServiceProvider;
8+
use WorkOS\Laravel\Services\WorkOSService;
69

710
/**
811
* Class WorkOSServiceProvider.
@@ -12,30 +15,42 @@ class WorkOSServiceProvider extends ServiceProvider
1215
/**
1316
* Bootstrap the ServiceProvider.
1417
*/
15-
public function boot()
18+
public function boot(): void
1619
{
1720
if ($this->app->runningInConsole()) {
1821
$this->publishes(
19-
[__DIR__."/../config/workos.php" => config_path("workos.php")]
22+
[__DIR__.'/../config/workos.php' => config_path('workos.php')],
23+
'workos-config'
2024
);
2125
}
2226
}
2327

2428
/**
2529
* Register the ServiceProvider as well as setup WorkOS.
2630
*/
27-
public function register()
31+
public function register(): void
2832
{
29-
$this->mergeConfigFrom(__DIR__."/../config/workos.php", "workos");
33+
$this->mergeConfigFrom(__DIR__.'/../config/workos.php', 'workos');
3034

31-
$config = $this->app["config"]->get("workos");
32-
\WorkOS\WorkOS::setApiKey($config["api_key"]);
33-
\WorkOS\WorkOS::setClientId($config["client_id"]);
34-
\WorkOS\WorkOS::setIdentifier(\WorkOS\Laravel\Version::SDK_IDENTIFIER);
35-
\WorkOS\WorkOS::setVersion(\WorkOS\Laravel\Version::SDK_VERSION);
35+
// Ensures that the WorkOS service is configured only once, rather than every request
36+
$this->app->singleton('workos', function ($app) {
37+
$config = $app['config']->get('workos');
3638

37-
if ($config["api_base_url"]) {
38-
\WorkOS\WorkOS::setApiBaseUrl($config["api_base_url"]);
39-
}
39+
\WorkOS\WorkOS::setApiKey($config['api_key']);
40+
\WorkOS\WorkOS::setClientId($config['client_id']);
41+
\WorkOS\WorkOS::setIdentifier(Version::SDK_IDENTIFIER);
42+
\WorkOS\WorkOS::setVersion(Version::SDK_VERSION);
43+
44+
if ($config['api_base_url']) {
45+
\WorkOS\WorkOS::setApiBaseUrl($config['api_base_url']);
46+
}
47+
48+
return new WorkOSService;
49+
});
50+
51+
// Allows for dependency injection (e.g. `show(WorkOSService $service)`)
52+
// while still ensuring we're using the configured singleton rather than
53+
// potentially generating a new, unconfigured version of the singleton
54+
$this->app->alias('workos', WorkOSService::class);
4055
}
4156
}

lib/helpers.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use WorkOS\Laravel\Services\WorkOSService;
6+
7+
if (! function_exists('workos')) {
8+
/**
9+
* Access the WorkOS Manager.
10+
*
11+
* @return \WorkOS\Laravel\WorkOSManager
12+
*/
13+
function workos()
14+
{
15+
return app(WorkOSService::class);
16+
}
17+
}

tests/WorkOS/WorkOSFacadeTest.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
namespace WorkOS\Laravel;
4+
5+
use WorkOS\Laravel\Facades\WorkOS;
6+
use WorkOS\Laravel\Services\WorkOSService;
7+
8+
class WorkOSFacadeTest extends LaravelTestCase
9+
{
10+
protected $app;
11+
12+
protected function setUp(): void
13+
{
14+
$this->app = $this->setupApplication();
15+
$this->setDefaultConfig();
16+
$this->setupProvider($this->app);
17+
}
18+
19+
protected function setDefaultConfig(array $overrides = []): void
20+
{
21+
$defaults = [
22+
'api_key' => 'pk_test',
23+
'client_id' => 'client_test',
24+
];
25+
26+
foreach (array_merge($defaults, $overrides) as $key => $value) {
27+
$this->app['config']->set("workos.{$key}", $value);
28+
}
29+
}
30+
31+
public function test_facade_resolves_workos_service()
32+
{
33+
WorkOS::setFacadeApplication($this->app);
34+
35+
$this->assertInstanceOf(\WorkOS\UserManagement::class, WorkOS::userManagement());
36+
}
37+
38+
public function test_facade_provides_access_to_all_services()
39+
{
40+
WorkOS::setFacadeApplication($this->app);
41+
42+
$this->assertInstanceOf(\WorkOS\AuditLogs::class, WorkOS::auditLogs());
43+
$this->assertInstanceOf(\WorkOS\DirectorySync::class, WorkOS::directorySync());
44+
$this->assertInstanceOf(\WorkOS\MFA::class, WorkOS::mfa());
45+
$this->assertInstanceOf(\WorkOS\Organizations::class, WorkOS::organizations());
46+
$this->assertInstanceOf(\WorkOS\Portal::class, WorkOS::portal());
47+
$this->assertInstanceOf(\WorkOS\SSO::class, WorkOS::sso());
48+
$this->assertInstanceOf(\WorkOS\UserManagement::class, WorkOS::userManagement());
49+
}
50+
}

tests/WorkOS/WorkOSServiceProviderTest.php

Lines changed: 88 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,104 @@
22

33
namespace WorkOS\Laravel;
44

5+
use WorkOS\Laravel\Services\WorkOSService;
6+
57
class WorkOSServiceProviderTest extends LaravelTestCase
68
{
79
protected $app;
810

911
protected function setUp(): void
1012
{
1113
$this->app = $this->setupApplication();
14+
$this->setDefaultConfig();
15+
$this->setupProvider($this->app);
1216
}
1317

14-
public function testRegisterWorkOSServiceProviderYieldsExpectedConfig()
18+
protected function setDefaultConfig(array $overrides = []): void
1519
{
16-
$this->app["config"]->set("workos.api_key", "pk_secretsauce");
17-
$this->app["config"]->set("workos.client_id", "client_pizza");
18-
$this->app["config"]->set("workos.api_base_url", "https://workos-hop.com/");
19-
$this->setupProvider($this->app);
20+
$defaults = [
21+
'api_key' => 'pk_test',
22+
'client_id' => 'client_test',
23+
];
24+
25+
foreach (array_merge($defaults, $overrides) as $key => $value) {
26+
$this->app['config']->set("workos.{$key}", $value);
27+
}
28+
}
29+
30+
public function test_register_work_os_service_provider_yields_expected_config()
31+
{
32+
$this->setDefaultConfig([
33+
'api_key' => 'pk_secretsauce',
34+
'client_id' => 'client_pizza',
35+
'api_base_url' => 'https://workos-hop.com/',
36+
]);
37+
38+
// Resolve the service to trigger lazy initialization
39+
$this->app->make('workos');
40+
41+
$this->assertEquals('pk_secretsauce', \WorkOS\WorkOS::getApiKey());
42+
$this->assertEquals('client_pizza', \WorkOS\WorkOS::getClientId());
43+
$this->assertEquals('https://workos-hop.com/', \WorkOS\WorkOS::getApiBaseUrl());
44+
}
45+
46+
public function test_workos_helper_function_returns_work_os_service_instance()
47+
{
48+
$this->assertInstanceOf(WorkOSService::class, workos());
49+
}
50+
51+
public function test_workos_helper_function_enables_fluent_access()
52+
{
53+
$this->assertInstanceOf(\WorkOS\UserManagement::class, workos()->userManagement());
54+
}
55+
56+
public function test_it_resolves_service_via_injection_and_configures_sdk()
57+
{
58+
$service = $this->app->make(WorkOSService::class);
59+
60+
$this->assertInstanceOf(WorkOSService::class, $service);
61+
$this->assertSame($service, $this->app->make('workos'));
62+
$this->assertSame($service, workos());
63+
}
64+
65+
public function test_workos_service_resolves_all_supported_services()
66+
{
67+
$service = workos();
68+
69+
$this->assertInstanceOf(\WorkOS\AuditLogs::class, $service->auditLogs());
70+
$this->assertInstanceOf(\WorkOS\DirectorySync::class, $service->directorySync());
71+
$this->assertInstanceOf(\WorkOS\MFA::class, $service->mfa());
72+
$this->assertInstanceOf(\WorkOS\Organizations::class, $service->organizations());
73+
$this->assertInstanceOf(\WorkOS\Portal::class, $service->portal());
74+
$this->assertInstanceOf(\WorkOS\SSO::class, $service->sso());
75+
$this->assertInstanceOf(\WorkOS\UserManagement::class, $service->userManagement());
76+
}
77+
78+
public function test_workos_service_caches_service_instances()
79+
{
80+
$service = workos();
81+
82+
$userManagement1 = $service->userManagement();
83+
$userManagement2 = $service->userManagement();
84+
85+
$this->assertSame($userManagement1, $userManagement2);
86+
}
87+
88+
public function test_workos_service_throws_exception_for_unsupported_service()
89+
{
90+
$this->expectException(\InvalidArgumentException::class);
91+
$this->expectExceptionMessage('WorkOS service [unsupportedService] is not supported.');
92+
93+
$service = workos();
94+
$service->unsupportedService();
95+
}
96+
97+
public function test_api_base_url_is_set_when_provided()
98+
{
99+
$this->setDefaultConfig(['api_base_url' => 'https://custom-api.workos.com/']);
100+
101+
$this->app->make('workos');
20102

21-
$this->assertEquals("pk_secretsauce", \WorkOS\WorkOS::getApiKey());
22-
$this->assertEquals("client_pizza", \WorkOS\WorkOS::getClientId());
23-
$this->assertEquals("https://workos-hop.com/", \WorkOS\WorkOS::getApiBaseUrl());
103+
$this->assertEquals('https://custom-api.workos.com/', \WorkOS\WorkOS::getApiBaseUrl());
24104
}
25105
}

0 commit comments

Comments
 (0)