From 0f977f31c0671aede067e76f8cdc4a1a223f802e Mon Sep 17 00:00:00 2001 From: Birdcar <434063+birdcar@users.noreply.github.com> Date: Thu, 11 Dec 2025 15:05:22 -0600 Subject: [PATCH 1/2] 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. --- composer.json | 15 +++- lib/Facades/WorkOS.php | 26 ++++++ lib/Services/WorkOSService.php | 71 ++++++++++++++++ lib/WorkOSServiceProvider.php | 39 ++++++--- lib/helpers.php | 17 ++++ tests/WorkOS/WorkOSFacadeTest.php | 50 +++++++++++ tests/WorkOS/WorkOSServiceProviderTest.php | 96 ++++++++++++++++++++-- 7 files changed, 291 insertions(+), 23 deletions(-) create mode 100644 lib/Facades/WorkOS.php create mode 100644 lib/Services/WorkOSService.php create mode 100644 lib/helpers.php create mode 100644 tests/WorkOS/WorkOSFacadeTest.php diff --git a/composer.json b/composer.json index 6fc4829..e94990f 100644 --- a/composer.json +++ b/composer.json @@ -19,11 +19,14 @@ ], "require": { "php": ">=8.1.0", + "illuminate/contracts": "^11.0|^12.0", + "illuminate/support": "^11.0|^12.0", "workos/workos-php": "^v4.29.0" }, "require-dev": { "friendsofphp/php-cs-fixer": "^2.15 || ^3.6", - "phpunit/phpunit": "^5.7 || ^10.1" + "phpunit/phpunit": "^5.7 || ^10.1", + "orchestra/testbench": "^9.0|^10.0" }, "suggest": { "laravel/framework": "For testing" @@ -31,7 +34,10 @@ "autoload": { "psr-4": { "WorkOS\\Laravel\\": "lib/" - } + }, + "files": [ + "lib/helpers.php" + ] }, "autoload-dev": { "psr-4": { @@ -45,7 +51,10 @@ "laravel": { "providers": [ "WorkOS\\Laravel\\WorkOSServiceProvider" - ] + ], + "aliases": { + "WorkOS": "WorkOS\\Laravel\\Facades\\WorkOS" + } } }, "scripts": { diff --git a/lib/Facades/WorkOS.php b/lib/Facades/WorkOS.php new file mode 100644 index 0000000..75fc84b --- /dev/null +++ b/lib/Facades/WorkOS.php @@ -0,0 +1,26 @@ + AuditLogs::class, + 'directorySync' => DirectorySync::class, + 'mfa' => MFA::class, + 'organizations' => Organizations::class, + 'portal' => Portal::class, + 'sso' => SSO::class, + 'userManagement' => UserManagement::class, + ]; + + /** + * Dynamically resolve a WorkOS service. + * + * @param string $name + * @param array $arguments + * @return mixed + */ + public function __call($name, $arguments) + { + if (! array_key_exists($name, $this->serviceMap)) { + throw new InvalidArgumentException("WorkOS service [$name] is not supported."); + } + + if (isset($this->instances[$name])) { + return $this->instances[$name]; + } + + return $this->instances[$name] = $arguments ? new $this->serviceMap[$name]($arguments) : new $this->serviceMap[$name]; + } +} diff --git a/lib/WorkOSServiceProvider.php b/lib/WorkOSServiceProvider.php index a9faa49..eacc908 100644 --- a/lib/WorkOSServiceProvider.php +++ b/lib/WorkOSServiceProvider.php @@ -1,8 +1,11 @@ app->runningInConsole()) { $this->publishes( - [__DIR__."/../config/workos.php" => config_path("workos.php")] + [__DIR__.'/../config/workos.php' => config_path('workos.php')], + 'workos-config' ); } } @@ -24,18 +28,29 @@ public function boot() /** * Register the ServiceProvider as well as setup WorkOS. */ - public function register() + public function register(): void { - $this->mergeConfigFrom(__DIR__."/../config/workos.php", "workos"); + $this->mergeConfigFrom(__DIR__.'/../config/workos.php', 'workos'); - $config = $this->app["config"]->get("workos"); - \WorkOS\WorkOS::setApiKey($config["api_key"]); - \WorkOS\WorkOS::setClientId($config["client_id"]); - \WorkOS\WorkOS::setIdentifier(\WorkOS\Laravel\Version::SDK_IDENTIFIER); - \WorkOS\WorkOS::setVersion(\WorkOS\Laravel\Version::SDK_VERSION); + // Ensures that the WorkOS service is configured only once, rather than every request + $this->app->singleton('workos', function ($app) { + $config = $app['config']->get('workos'); - if ($config["api_base_url"]) { - \WorkOS\WorkOS::setApiBaseUrl($config["api_base_url"]); - } + \WorkOS\WorkOS::setApiKey($config['api_key']); + \WorkOS\WorkOS::setClientId($config['client_id']); + \WorkOS\WorkOS::setIdentifier(Version::SDK_IDENTIFIER); + \WorkOS\WorkOS::setVersion(Version::SDK_VERSION); + + if ($config['api_base_url']) { + \WorkOS\WorkOS::setApiBaseUrl($config['api_base_url']); + } + + return new WorkOSService; + }); + + // Allows for dependency injection (e.g. `show(WorkOSService $service)`) + // while still ensuring we're using the configured singleton rather than + // potentially generating a new, unconfigured version of the singleton + $this->app->alias('workos', WorkOSService::class); } } diff --git a/lib/helpers.php b/lib/helpers.php new file mode 100644 index 0000000..770399c --- /dev/null +++ b/lib/helpers.php @@ -0,0 +1,17 @@ +app = $this->setupApplication(); + $this->setDefaultConfig(); + $this->setupProvider($this->app); + } + + protected function setDefaultConfig(array $overrides = []): void + { + $defaults = [ + 'api_key' => 'pk_test', + 'client_id' => 'client_test', + ]; + + foreach (array_merge($defaults, $overrides) as $key => $value) { + $this->app['config']->set("workos.{$key}", $value); + } + } + + public function test_facade_resolves_workos_service() + { + WorkOS::setFacadeApplication($this->app); + + $this->assertInstanceOf(\WorkOS\UserManagement::class, WorkOS::userManagement()); + } + + public function test_facade_provides_access_to_all_services() + { + WorkOS::setFacadeApplication($this->app); + + $this->assertInstanceOf(\WorkOS\AuditLogs::class, WorkOS::auditLogs()); + $this->assertInstanceOf(\WorkOS\DirectorySync::class, WorkOS::directorySync()); + $this->assertInstanceOf(\WorkOS\MFA::class, WorkOS::mfa()); + $this->assertInstanceOf(\WorkOS\Organizations::class, WorkOS::organizations()); + $this->assertInstanceOf(\WorkOS\Portal::class, WorkOS::portal()); + $this->assertInstanceOf(\WorkOS\SSO::class, WorkOS::sso()); + $this->assertInstanceOf(\WorkOS\UserManagement::class, WorkOS::userManagement()); + } +} diff --git a/tests/WorkOS/WorkOSServiceProviderTest.php b/tests/WorkOS/WorkOSServiceProviderTest.php index 5d91826..661a035 100644 --- a/tests/WorkOS/WorkOSServiceProviderTest.php +++ b/tests/WorkOS/WorkOSServiceProviderTest.php @@ -2,6 +2,8 @@ namespace WorkOS\Laravel; +use WorkOS\Laravel\Services\WorkOSService; + class WorkOSServiceProviderTest extends LaravelTestCase { protected $app; @@ -9,17 +11,95 @@ class WorkOSServiceProviderTest extends LaravelTestCase protected function setUp(): void { $this->app = $this->setupApplication(); + $this->setDefaultConfig(); + $this->setupProvider($this->app); } - public function testRegisterWorkOSServiceProviderYieldsExpectedConfig() + protected function setDefaultConfig(array $overrides = []): void { - $this->app["config"]->set("workos.api_key", "pk_secretsauce"); - $this->app["config"]->set("workos.client_id", "client_pizza"); - $this->app["config"]->set("workos.api_base_url", "https://workos-hop.com/"); - $this->setupProvider($this->app); + $defaults = [ + 'api_key' => 'pk_test', + 'client_id' => 'client_test', + ]; + + foreach (array_merge($defaults, $overrides) as $key => $value) { + $this->app['config']->set("workos.{$key}", $value); + } + } + + public function test_register_work_os_service_provider_yields_expected_config() + { + $this->setDefaultConfig([ + 'api_key' => 'pk_secretsauce', + 'client_id' => 'client_pizza', + 'api_base_url' => 'https://workos-hop.com/', + ]); + + // Resolve the service to trigger lazy initialization + $this->app->make('workos'); + + $this->assertEquals('pk_secretsauce', \WorkOS\WorkOS::getApiKey()); + $this->assertEquals('client_pizza', \WorkOS\WorkOS::getClientId()); + $this->assertEquals('https://workos-hop.com/', \WorkOS\WorkOS::getApiBaseUrl()); + } + + public function test_workos_helper_function_returns_work_os_service_instance() + { + $this->assertInstanceOf(WorkOSService::class, workos()); + } + + public function test_workos_helper_function_enables_fluent_access() + { + $this->assertInstanceOf(\WorkOS\UserManagement::class, workos()->userManagement()); + } + + public function test_it_resolves_service_via_injection_and_configures_sdk() + { + $service = $this->app->make(WorkOSService::class); + + $this->assertInstanceOf(WorkOSService::class, $service); + $this->assertSame($service, $this->app->make('workos')); + $this->assertSame($service, workos()); + } + + public function test_workos_service_resolves_all_supported_services() + { + $service = workos(); + + $this->assertInstanceOf(\WorkOS\AuditLogs::class, $service->auditLogs()); + $this->assertInstanceOf(\WorkOS\DirectorySync::class, $service->directorySync()); + $this->assertInstanceOf(\WorkOS\MFA::class, $service->mfa()); + $this->assertInstanceOf(\WorkOS\Organizations::class, $service->organizations()); + $this->assertInstanceOf(\WorkOS\Portal::class, $service->portal()); + $this->assertInstanceOf(\WorkOS\SSO::class, $service->sso()); + $this->assertInstanceOf(\WorkOS\UserManagement::class, $service->userManagement()); + } + + public function test_workos_service_caches_service_instances() + { + $service = workos(); + + $userManagement1 = $service->userManagement(); + $userManagement2 = $service->userManagement(); + + $this->assertSame($userManagement1, $userManagement2); + } + + public function test_workos_service_throws_exception_for_unsupported_service() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('WorkOS service [unsupportedService] is not supported.'); + + $service = workos(); + $service->unsupportedService(); + } + + public function test_api_base_url_is_set_when_provided() + { + $this->setDefaultConfig(['api_base_url' => 'https://custom-api.workos.com/']); + + $this->app->make('workos'); - $this->assertEquals("pk_secretsauce", \WorkOS\WorkOS::getApiKey()); - $this->assertEquals("client_pizza", \WorkOS\WorkOS::getClientId()); - $this->assertEquals("https://workos-hop.com/", \WorkOS\WorkOS::getApiBaseUrl()); + $this->assertEquals('https://custom-api.workos.com/', \WorkOS\WorkOS::getApiBaseUrl()); } } From b0896e4196e7d082137a1ae6d33595e4f94a27b4 Mon Sep 17 00:00:00 2001 From: Birdcar <434063+birdcar@users.noreply.github.com> Date: Thu, 11 Dec 2025 16:29:30 -0600 Subject: [PATCH 2/2] Fix dependency issues with older versions of laravel --- composer.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index e94990f..330abf6 100644 --- a/composer.json +++ b/composer.json @@ -19,14 +19,14 @@ ], "require": { "php": ">=8.1.0", - "illuminate/contracts": "^11.0|^12.0", - "illuminate/support": "^11.0|^12.0", + "illuminate/contracts": "^10.0|^11.0|^12.0", + "illuminate/support": "^10.0|^11.0|^12.0", "workos/workos-php": "^v4.29.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^2.15 || ^3.6", - "phpunit/phpunit": "^5.7 || ^10.1", - "orchestra/testbench": "^9.0|^10.0" + "friendsofphp/php-cs-fixer": "^3.64", + "phpunit/phpunit": "^10.1 || ^11.0", + "orchestra/testbench": "^8.0|^9.0|^10.0" }, "suggest": { "laravel/framework": "For testing"