diff --git a/src/Configuration/EnvConfiguration.php b/src/Configuration/EnvConfiguration.php index 2ca6a22d..ffdc744c 100644 --- a/src/Configuration/EnvConfiguration.php +++ b/src/Configuration/EnvConfiguration.php @@ -13,13 +13,11 @@ class EnvConfiguration extends Configuration */ public function create(Loader $config): void { - $this->container->bind('env', function () use ($config) { - Env::configure($config['app.env_file'] ?? null); + Env::configure($config->getPath('.env.json') ?? null); - $event = Env::getInstance(); + $event = Env::getInstance(); - $this->container->instance('env', $event); - }); + $this->container->instance('env', $event); } /** @@ -27,6 +25,6 @@ public function create(Loader $config): void */ public function run(): void { - // $this->container->make('env'); + // } } diff --git a/src/Configuration/Loader.php b/src/Configuration/Loader.php index 03463fe9..8402e71f 100644 --- a/src/Configuration/Loader.php +++ b/src/Configuration/Loader.php @@ -29,6 +29,11 @@ class Loader implements ArrayAccess */ protected string $base_path; + /** + * @var string + */ + protected string $config_path; + /** * @var bool */ @@ -56,6 +61,7 @@ class Loader implements ArrayAccess private function __construct(string $base_path) { $this->base_path = $base_path; + $this->config_path = $base_path . DIRECTORY_SEPARATOR . 'config'; $this->config = new Arraydotify([]); } @@ -85,6 +91,17 @@ public function isCli(): bool return php_sapi_name() == 'cli'; } + /** + * Get the base path + * + * @param string $filename + * @return string + */ + public function getPath(string $filename): string + { + return $this->base_path . DIRECTORY_SEPARATOR . $filename; + } + /** * Get the base path * @@ -95,6 +112,19 @@ public function getBasePath(): string return $this->base_path; } + /** + * Set the configuration path + * + * @param string $path + * @return Loader + */ + public function withConfigPath(string $path): Loader + { + $this->config_path = $path; + + return $this; + } + /** * Middleware collection * @@ -177,9 +207,7 @@ public function boot(): Loader $container = Capsule::getInstance(); // Load the env configuration first - $env_config = $this->createConfiguration(EnvConfiguration::class, $container); - - $env_config->run(); + $this->createConfiguration(EnvConfiguration::class, $container); // Load the .env or .env.json file $this->loadConfigFiles(); @@ -193,7 +221,7 @@ public function boot(): Loader // Load configurations $this->runConfirmations($loaded_configurations); - // Load load events + // Load events $this->loadEvents(); // Set the load as booted @@ -284,7 +312,7 @@ private function loadConfigFiles(): void /** * We load all Bow configuration */ - $glob = glob($this->base_path . '/**.php'); + $glob = glob($this->config_path . '/**.php'); $config = []; diff --git a/src/Notifier/Adapters/SmsChannelAdapter.php b/src/Notifier/Adapters/SmsChannelAdapter.php index eedc44a3..a129d5b6 100644 --- a/src/Notifier/Adapters/SmsChannelAdapter.php +++ b/src/Notifier/Adapters/SmsChannelAdapter.php @@ -39,7 +39,7 @@ class SmsChannelAdapter implements ChannelAdapterInterface public function __construct() { $config = config('notifier.sms'); - $this->setting = $config['setting'] ?? []; + $this->setting = $config; $this->sms_provider = $config['provider'] ?? 'callisto'; } @@ -77,10 +77,11 @@ public function send(Model $context, Notifier $notifier): void private function sendWithTwilio(Model $context, Notifier $notifier): void { $data = $notifier->toSms($context); + $config = $this->setting['twilio'] ?? []; - $account_sid = $this->setting['account_sid'] ?? null; - $auth_token = $this->setting['auth_token'] ?? null; - $this->from_number = $this->setting['from'] ?? null; + $account_sid = $config['account_sid'] ?? null; + $auth_token = $config['auth_token'] ?? null; + $this->from_number = $config['from'] ?? null; if (!$account_sid || !$auth_token || !$this->from_number) { throw new InvalidArgumentException('Twilio credentials are required'); @@ -110,9 +111,12 @@ private function sendWithTwilio(Model $context, Notifier $notifier): void */ private function sendWithCallisto(Model $context, Notifier $notifier): void { - $access_key = $this->setting['access_key'] ?? null; - $access_secret = $this->setting['access_secret'] ?? null; - $notify_url = $this->setting['notify_url'] ?? null; + $config = $this->setting['callisto'] ?? []; + + $access_key = $config['access_key'] ?? null; + $access_secret = $config['access_secret'] ?? null; + $notify_url = $config['notify_url'] ?? null; + $sender = $config['sender'] ?? null; if (!$access_key || !$access_secret) { throw new InvalidArgumentException('Callisto credentials are required'); @@ -120,7 +124,7 @@ private function sendWithCallisto(Model $context, Notifier $notifier): void $data = $notifier->toSms($context); - if (!isset($data['to']) || !isset($data['message']) || !isset($data['sender'])) { + if (!isset($data['to']) || !isset($data['message'])) { throw new InvalidArgumentException('The phone number and notifier are required'); } @@ -133,7 +137,7 @@ private function sendWithCallisto(Model $context, Notifier $notifier): void $payload = [ 'to' => (array) $data['to'], 'message' => $data['message'], - 'sender' => $data['sender'], + 'sender' => $data['sender'] ?? $sender, ]; if ($data['notify_url']) { diff --git a/src/Support/Env.php b/src/Support/Env.php index 3aeb5635..7ed992b1 100644 --- a/src/Support/Env.php +++ b/src/Support/Env.php @@ -84,6 +84,10 @@ public function __construct(string $filename) */ public static function configure(string $filename) { + if (static::$instance !== null) { + return; + } + if (!file_exists($filename)) { throw new InvalidArgumentException( "The application environment file [.env.json] cannot be empty or is not define." diff --git a/tests/Config/ConfigurationTest.php b/tests/Config/ConfigurationTest.php index db5a4d80..6eb4cf64 100644 --- a/tests/Config/ConfigurationTest.php +++ b/tests/Config/ConfigurationTest.php @@ -165,7 +165,7 @@ public function test_invoke_method() public function test_get_base_path() { $basePath = $this->config->getBasePath(); - $this->assertEquals(__DIR__ . '/stubs/config', $basePath); + $this->assertEquals(__DIR__ . '/stubs', $basePath); $this->assertIsString($basePath); } diff --git a/tests/Config/TestingConfiguration.php b/tests/Config/TestingConfiguration.php index 7982de6b..50aecfe1 100644 --- a/tests/Config/TestingConfiguration.php +++ b/tests/Config/TestingConfiguration.php @@ -58,6 +58,6 @@ public static function getConfig(): ConfigurationLoader { Env::configure(__DIR__ . '/stubs/env.json'); - return KernelTesting::configure(__DIR__ . '/stubs/config')->boot(); + return KernelTesting::configure(__DIR__ . '/stubs')->withConfigPath(__DIR__ . '/stubs/config')->boot(); } } diff --git a/tests/Container/CapsuleTest.php b/tests/Container/CapsuleTest.php index d0c91124..08fc65c2 100644 --- a/tests/Container/CapsuleTest.php +++ b/tests/Container/CapsuleTest.php @@ -3,7 +3,14 @@ namespace Bow\Tests\Container; use Bow\Container\Capsule; +use Bow\Tests\Container\Stubs\FileLogger; +use Bow\Tests\Container\Stubs\LoggerInterface; use Bow\Tests\Container\Stubs\MyClass; +use Bow\Tests\Container\Stubs\OrderService; +use Bow\Tests\Container\Stubs\PaymentGatewayInterface; +use Bow\Tests\Container\Stubs\PaypalPaymentGateway; +use Bow\Tests\Container\Stubs\SimpleService; +use Bow\Tests\Container\Stubs\StripePaymentGateway; use StdClass; class CapsuleTest extends \PHPUnit\Framework\TestCase @@ -40,4 +47,244 @@ public function test_make_my_class_container() $this->assertInstanceOf(MyClass::class, $my_class); $this->assertInstanceOf(\Bow\Support\Collection::class, $my_class->getCollection()); } + + public function test_bind_interface_to_concrete_implementation() + { + $capsule = new Capsule(); + $capsule->bind(PaymentGatewayInterface::class, fn() => new StripePaymentGateway()); + + $gateway = $capsule->make(PaymentGatewayInterface::class); + + $this->assertInstanceOf(PaymentGatewayInterface::class, $gateway); + $this->assertInstanceOf(StripePaymentGateway::class, $gateway); + $this->assertEquals('stripe', $gateway->getName()); + } + + public function test_bind_interface_to_different_implementation() + { + $capsule = new Capsule(); + $capsule->bind(PaymentGatewayInterface::class, fn() => new PaypalPaymentGateway()); + + $gateway = $capsule->make(PaymentGatewayInterface::class); + + $this->assertInstanceOf(PaymentGatewayInterface::class, $gateway); + $this->assertInstanceOf(PaypalPaymentGateway::class, $gateway); + $this->assertEquals('paypal', $gateway->getName()); + } + + public function test_bind_multiple_interfaces() + { + $capsule = new Capsule(); + $capsule->bind(PaymentGatewayInterface::class, fn() => new StripePaymentGateway()); + $capsule->bind(LoggerInterface::class, fn() => new FileLogger()); + + $gateway = $capsule->make(PaymentGatewayInterface::class); + $logger = $capsule->make(LoggerInterface::class); + + $this->assertInstanceOf(StripePaymentGateway::class, $gateway); + $this->assertInstanceOf(FileLogger::class, $logger); + } + + public function test_auto_resolve_dependencies_with_interfaces() + { + $capsule = new Capsule(); + $capsule->bind(PaymentGatewayInterface::class, fn() => new StripePaymentGateway()); + $capsule->bind(LoggerInterface::class, fn() => new FileLogger()); + $capsule->bind(OrderService::class, fn(Capsule $c) => new OrderService( + $c->make(PaymentGatewayInterface::class), + $c->make(LoggerInterface::class) + )); + + $orderService = $capsule->make(OrderService::class); + + $this->assertInstanceOf(OrderService::class, $orderService); + $this->assertInstanceOf(StripePaymentGateway::class, $orderService->getPaymentGateway()); + $this->assertInstanceOf(FileLogger::class, $orderService->getLogger()); + } + + public function test_injected_service_is_functional() + { + $capsule = new Capsule(); + $capsule->bind(PaymentGatewayInterface::class, fn() => new StripePaymentGateway()); + $capsule->bind(LoggerInterface::class, fn() => new FileLogger()); + $capsule->bind(OrderService::class, fn(Capsule $c) => new OrderService( + $c->make(PaymentGatewayInterface::class), + $c->make(LoggerInterface::class) + )); + + $orderService = $capsule->make(OrderService::class); + $result = $orderService->processOrder(100.00); + + $this->assertTrue($result); + $this->assertCount(1, $orderService->getLogger()->getMessages()); + } + + public function test_instance_returns_same_object() + { + $capsule = new Capsule(); + $logger = new FileLogger(); + $capsule->instance(LoggerInterface::class, $logger); + + $resolved1 = $capsule->make(LoggerInterface::class); + $resolved2 = $capsule->make(LoggerInterface::class); + + $this->assertSame($resolved1, $resolved2); + $this->assertSame($logger, $resolved1); + } + + public function test_instance_preserves_state() + { + $capsule = new Capsule(); + $logger = new FileLogger(); + $capsule->instance(LoggerInterface::class, $logger); + + $resolved = $capsule->make(LoggerInterface::class); + $resolved->log('First message'); + + $resolvedAgain = $capsule->make(LoggerInterface::class); + + $this->assertCount(1, $resolvedAgain->getMessages()); + $this->assertEquals('[FILE] First message', $resolvedAgain->getMessages()[0]); + } + + public function test_factory_creates_new_instance_each_time() + { + $capsule = new Capsule(); + $capsule->factory(LoggerInterface::class, fn() => new FileLogger()); + + $logger1 = $capsule->make(LoggerInterface::class); + $logger1->log('Message 1'); + + $logger2 = $capsule->make(LoggerInterface::class); + + $this->assertNotSame($logger1, $logger2); + $this->assertCount(1, $logger1->getMessages()); + $this->assertCount(0, $logger2->getMessages()); + } + + public function test_factory_with_container_injection() + { + $capsule = new Capsule(); + $capsule->bind(PaymentGatewayInterface::class, fn() => new StripePaymentGateway()); + $capsule->factory('payment-processor', fn(Capsule $c) => $c->make(PaymentGatewayInterface::class)); + + $processor = $capsule->make('payment-processor'); + + $this->assertInstanceOf(StripePaymentGateway::class, $processor); + } + + public function test_array_access_offset_exists() + { + $capsule = new Capsule(); + $capsule->bind('existing-key', fn() => new StdClass()); + + $this->assertTrue(isset($capsule['existing-key'])); + $this->assertFalse(isset($capsule['non-existing-key'])); + } + + public function test_array_access_offset_get() + { + $capsule = new Capsule(); + $capsule->bind('test-key', fn() => new StripePaymentGateway()); + + $result = $capsule['test-key']; + + $this->assertInstanceOf(StripePaymentGateway::class, $result); + } + + public function test_array_access_offset_set() + { + $capsule = new Capsule(); + $capsule['custom-service'] = fn() => new FileLogger(); + + $result = $capsule->make('custom-service'); + + $this->assertInstanceOf(FileLogger::class, $result); + } + + public function test_array_access_offset_unset() + { + $capsule = new Capsule(); + $capsule->bind('removable', fn() => new StdClass()); + + $this->assertTrue(isset($capsule['removable'])); + + unset($capsule['removable']); + + // After unset, the key still exists in cache but the register is removed + // Attempting to resolve will try to instantiate "removable" as a class + $this->expectException(\ReflectionException::class); + $capsule->make('removable'); + } + + public function test_make_with_parameters() + { + $capsule = new Capsule(); + + $service = $capsule->makeWith(SimpleService::class, ['custom-name']); + + $this->assertInstanceOf(SimpleService::class, $service); + $this->assertEquals('custom-name', $service->getName()); + } + + public function test_make_with_default_parameters() + { + $capsule = new Capsule(); + + $service = $capsule->make(SimpleService::class); + + $this->assertInstanceOf(SimpleService::class, $service); + $this->assertEquals('default', $service->getName()); + } + + public function test_bind_returns_capsule_for_chaining() + { + $capsule = new Capsule(); + + $result = $capsule + ->bind(PaymentGatewayInterface::class, fn() => new StripePaymentGateway()) + ->bind(LoggerInterface::class, fn() => new FileLogger()); + + $this->assertInstanceOf(Capsule::class, $result); + } + + public function test_factory_returns_capsule_for_chaining() + { + $capsule = new Capsule(); + + $result = $capsule + ->factory('service1', fn() => new StdClass()) + ->factory('service2', fn() => new StdClass()); + + $this->assertInstanceOf(Capsule::class, $result); + } + + public function test_instance_returns_capsule_for_chaining() + { + $capsule = new Capsule(); + + $result = $capsule + ->instance('logger', new FileLogger()) + ->instance('gateway', new StripePaymentGateway()); + + $this->assertInstanceOf(Capsule::class, $result); + } + + public function test_get_instance_returns_singleton() + { + $instance1 = Capsule::getInstance(); + $instance2 = Capsule::getInstance(); + + $this->assertSame($instance1, $instance2); + } + + public function test_bind_with_class_name_string() + { + $capsule = new Capsule(); + $capsule->bind('payment', StripePaymentGateway::class); + + $result = $capsule->make('payment'); + + $this->assertInstanceOf(StripePaymentGateway::class, $result); + } } diff --git a/tests/Container/Stubs/FileLogger.php b/tests/Container/Stubs/FileLogger.php new file mode 100644 index 00000000..49edc699 --- /dev/null +++ b/tests/Container/Stubs/FileLogger.php @@ -0,0 +1,27 @@ +messages[] = '[FILE] ' . $message; + } + + /** + * @inheritDoc + */ + public function getMessages(): array + { + return $this->messages; + } +} diff --git a/tests/Container/Stubs/LoggerInterface.php b/tests/Container/Stubs/LoggerInterface.php new file mode 100644 index 00000000..93aaa5ac --- /dev/null +++ b/tests/Container/Stubs/LoggerInterface.php @@ -0,0 +1,21 @@ +paymentGateway = $paymentGateway; + $this->logger = $logger; + } + + /** + * Get the payment gateway + * + * @return PaymentGatewayInterface + */ + public function getPaymentGateway(): PaymentGatewayInterface + { + return $this->paymentGateway; + } + + /** + * Get the logger + * + * @return LoggerInterface + */ + public function getLogger(): LoggerInterface + { + return $this->logger; + } + + /** + * Process an order + * + * @param float $amount + * @return bool + */ + public function processOrder(float $amount): bool + { + $this->logger->log("Processing order for amount: {$amount}"); + + return $this->paymentGateway->process($amount); + } +} diff --git a/tests/Container/Stubs/PaymentGatewayInterface.php b/tests/Container/Stubs/PaymentGatewayInterface.php new file mode 100644 index 00000000..ac25f38a --- /dev/null +++ b/tests/Container/Stubs/PaymentGatewayInterface.php @@ -0,0 +1,21 @@ += 1.0; + } + + /** + * @inheritDoc + */ + public function getName(): string + { + return 'paypal'; + } +} diff --git a/tests/Container/Stubs/SimpleService.php b/tests/Container/Stubs/SimpleService.php new file mode 100644 index 00000000..097aed28 --- /dev/null +++ b/tests/Container/Stubs/SimpleService.php @@ -0,0 +1,31 @@ +name = $name; + } + + /** + * Get the service name + * + * @return string + */ + public function getName(): string + { + return $this->name; + } +} diff --git a/tests/Container/Stubs/StripePaymentGateway.php b/tests/Container/Stubs/StripePaymentGateway.php new file mode 100644 index 00000000..163fa6ab --- /dev/null +++ b/tests/Container/Stubs/StripePaymentGateway.php @@ -0,0 +1,22 @@ + 0; + } + + /** + * @inheritDoc + */ + public function getName(): string + { + return 'stripe'; + } +}