Add: adding support for laravel inspired serviceprovider

This commit is contained in:
Patrick Stewart 2024-10-02 19:18:06 -07:00
parent 0915c48054
commit 80a8b135c7
15 changed files with 3967 additions and 0 deletions

7
core/support/.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
# https://dart.dev/guides/libraries/private-files
# Created by `dart pub`
.dart_tool/
# Avoid committing pubspec.lock for library packages; see
# https://dart.dev/guides/libraries/private-files#pubspeclock.
pubspec.lock

View file

@ -0,0 +1,3 @@
## 1.0.0
- Initial version.

10
core/support/LICENSE.md Normal file
View file

@ -0,0 +1,10 @@
The MIT License (MIT)
The Laravel Framework is Copyright (c) Taylor Otwell
The Fabric Framework is Copyright (c) Vieo, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

1
core/support/README.md Normal file
View file

@ -0,0 +1 @@
<p align="center"><a href="https://protevus.com" target="_blank"><img src="https://git.protevus.com/protevus/branding/raw/branch/main/protevus-logo-bg.png"></a></p>

View file

@ -0,0 +1,30 @@
# This file configures the static analysis results for your project (errors,
# warnings, and lints).
#
# This enables the 'recommended' set of lints from `package:lints`.
# This set helps identify many issues that may lead to problems when running
# or consuming Dart code, and enforces writing Dart using a single, idiomatic
# style and format.
#
# If you want a smaller set of lints you can change this to specify
# 'package:lints/core.yaml'. These are just the most critical lints
# (the recommended set includes the core lints).
# The core lints are also what is used by pub.dev for scoring packages.
include: package:lints/recommended.yaml
# Uncomment the following section to specify additional rules.
# linter:
# rules:
# - camel_case_types
# analyzer:
# exclude:
# - path/to/excluded/files/**
# For more information about the core and recommended set of lints, see
# https://dart.dev/go/core-lints
# For additional information about configuring this file, see
# https://dart.dev/guides/language/analysis-options

8
core/support/build.yaml Normal file
View file

@ -0,0 +1,8 @@
targets:
$default:
builders:
reflectable:
generate_for:
- test/**.dart
options:
formatted: true

View file

@ -0,0 +1,13 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
library;
export '../src/service_provider.dart';
export '../src/discoverable_service_provider.dart';

View file

@ -0,0 +1,54 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'package:reflectable/reflectable.dart';
/// A reflectable class that provides capabilities for discovering services.
///
/// This class extends [Reflectable] and enables invoking, typing, and reflected type capabilities.
/// It is used to facilitate service discovery and reflection-based operations in the application.
///
/// The capabilities provided by this class are:
/// - [invokingCapability]: Allows invoking methods on reflected instances.
/// - [typingCapability]: Enables access to type information of reflected instances.
/// - [reflectedTypeCapability]: Provides access to the reflected type of instances.
/// - [newInstanceCapability]: Allows creation of new instances of reflected types.
///
/// Usage:
/// ```dart
/// const provider = DiscoverableServiceProvider();
/// // Use the provider to perform reflection-based operations
/// ```
///
/// This class is typically used in conjunction with the [discoverableServiceProvider]
/// constant to provide a shared instance for reflection operations throughout the application.
class DiscoverableServiceProvider extends Reflectable {
const DiscoverableServiceProvider()
: super(
invokingCapability,
typingCapability,
declarationsCapability,
reflectedTypeCapability,
newInstanceCapability,
superclassQuantifyCapability);
}
/// A constant instance of [DiscoverableServiceProvider].
///
/// This constant provides a single, shared instance of the [DiscoverableServiceProvider] class.
/// It can be used throughout the application to access the reflection capabilities
/// defined in the [DiscoverableServiceProvider] class, such as invoking methods,
/// accessing type information, and working with reflected types.
///
/// Usage:
/// ```dart
/// var reflector = discoverableServiceProvider;
/// // Use the reflector to perform reflection operations
/// ```
const discoverableServiceProvider = DiscoverableServiceProvider();

View file

@ -0,0 +1,887 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'package:angel3_framework/angel3_framework.dart';
import 'package:angel3_container/angel3_container.dart';
import 'package:meta/meta.dart';
import 'package:logging/logging.dart';
import 'package:reflectable/reflectable.dart';
import 'discoverable_service_provider.dart';
@discoverableServiceProvider
abstract class ServiceProvider {
/// Static variable to hold the logger instance for the ServiceProvider class.
/// This logger is lazily initialized and can be accessed via the `logger` getter.
/// It can also be set explicitly for testing purposes using the `setLogger` method.
static Logger? _logger;
/// Returns the logger for the ServiceProvider class.
///
/// If a logger has not been explicitly set using the `setLogger` method,
/// this getter creates and returns a new Logger instance with the name 'ServiceProvider'.
///
/// If a logger has been set, it returns that logger instance.
///
/// This approach allows for lazy initialization of the logger and provides
/// flexibility for testing by allowing the logger to be explicitly set when needed.
static Logger get logger => _logger ?? Logger('ServiceProvider');
/// Constructs a [ServiceProvider] instance.
///
/// The [priority] parameter determines the order in which service providers are registered.
/// Service providers with higher priority values are registered before those with lower priority values.
/// By default, the priority is set to 0.
///
/// Example:
/// ```dart
/// class MyServiceProvider extends ServiceProvider {
/// MyServiceProvider() : super(priority: 10);
/// }
/// ```
const ServiceProvider({this.priority = 0});
/// The priority of this service provider. Higher priority providers are registered first.
///
/// This integer value determines the order in which service providers are registered
/// within the Angel. Service providers with higher priority values are processed
/// before those with lower priority values. This allows for fine-grained control over
/// the initialization sequence of services, ensuring that dependencies are properly
/// set up before they are needed by other components.
///
/// For example, a service provider with a priority of 10 will be registered before
/// a service provider with a priority of 5. If two service providers have the same
/// priority, their registration order is not guaranteed.
final int priority;
/// Sets a custom logger for the ServiceProvider class.
///
/// This method allows changing the logger used by the ServiceProvider class,
/// which is particularly useful for testing purposes. It replaces the default
/// logger with the provided custom logger.
///
/// Parameters:
/// - logger: The custom Logger instance to be used.
///
/// Example:
/// ```dart
/// ServiceProvider.setLogger(MockLogger());
/// ```
///
/// Note: This method is marked as `@visibleForTesting` to indicate that it
/// should only be used in test environments.
@visibleForTesting
static void setLogger(Logger logger) {
_logger = logger;
print('Logger set to: $logger'); // Add this line
}
/// Registers the service provider with the given container and Angel.
///
/// This method is responsible for registering the service provider's dependencies
/// and services into the provided [container]. It also has access to the [app]
/// instance for any Angel-specific configurations.
///
/// The method is annotated with [@mustCallSuper], indicating that subclasses
/// overriding this method must call the superclass implementation.
///
/// This implementation logs the registration process, performs a sample
/// registration (which should be replaced with actual logic in subclasses),
/// and handles any exceptions that occur during the registration process.
///
/// Parameters:
/// - [container]: The dependency injection container to register with.
/// - [app]: The Angel instance, providing access to app-wide configurations.
///
/// Throws:
/// Any exceptions caught during the registration process are logged
/// and then re-thrown.
@mustCallSuper
void register(Container container, Angel app) {
try {
logger.info('Registering ${runtimeType.toString()}');
// Actual registration logic
container.registerSingleton<String>('test', as: null);
} catch (e, stackTrace) {
print('Caught exception in register: $e'); // Add this line
logger.severe(
'Error registering ${runtimeType.toString()}', e, stackTrace);
print('Logged severe message in register'); // Add this line
rethrow;
}
}
/// Boots the service provider, performing necessary setup tasks.
///
/// This method is responsible for initializing the service provider and
/// performing any required setup tasks. It's called during the Angel's
/// boot process.
///
/// The method is annotated with [@mustCallSuper], indicating that subclasses
/// overriding this method must call the superclass implementation.
///
/// This implementation logs the booting process, performs a sample
/// configuration (which should be replaced with actual logic in subclasses),
/// and handles any exceptions that occur during the booting process.
///
/// Parameters:
/// - [app]: The Angel instance, providing access to app-wide configurations.
///
/// Throws:
/// Any exceptions caught during the booting process are logged
/// and then re-thrown.
///
/// Returns:
/// A [Future] that completes when the boot process is finished.
@mustCallSuper
Future<void> boot(Angel app) async {
try {
logger.info('Booting ${runtimeType.toString()}');
// Actual booting logic
await app.configure((_) => null);
} catch (e, stackTrace) {
print('Caught exception in boot: $e'); // Add this line
logger.severe('Error booting ${runtimeType.toString()}', e, stackTrace);
print('Logged severe message in boot'); // Add this line
rethrow;
}
}
/// Indicates whether the service provider is deferred.
///
/// This getter returns a boolean value that determines if the service provider
/// should be loaded in a deferred manner. By default, it returns `false`,
/// meaning the service provider is not deferred.
///
/// Deferred loading can be useful for optimizing Angel startup time
/// by delaying the loading of certain services until they are actually needed.
///
/// Subclasses can override this getter to implement custom deferral logic.
///
/// Returns:
/// A boolean value. `false` indicates the service provider is not deferred,
/// while `true` would indicate it is deferred.
bool get isDeferred => false;
/// Returns a list of types that this service provider provides.
///
/// This getter is intended to be overridden by subclasses to specify
/// the types of services or dependencies that the service provider
/// makes available to the Angel.
///
/// By default, it returns an empty list, indicating that the base
/// ServiceProvider class doesn't provide any specific types.
///
/// Subclasses should override this getter to return a list of
/// [Type] objects representing the services they provide.
///
/// Example:
/// ```dart
/// @override
/// List<Type> get provides => [DatabaseService, LoggingService];
/// ```
///
/// Returns:
/// A [List] of [Type] objects representing the provided services.
List<Type> get provides => const [];
/// Determines whether the service provider should be registered.
///
/// This method is used to conditionally register the service provider based on
/// the current state of the Angel. By default, it always returns `true`,
/// meaning the service provider will be registered in all cases.
///
/// Subclasses can override this method to implement custom logic for determining
/// when the service provider should be registered. This can be useful for
/// conditional registration based on environment variables, configuration settings,
/// or other Angel-specific criteria.
///
/// Parameters:
/// - [app]: The current [Angel] instance, which can be used to access
/// Angel-wide settings or configurations.
///
/// Returns:
/// A boolean value. `true` indicates that the service provider should be
/// registered, while `false` indicates it should not be registered.
///
/// Example:
/// ```dart
/// @override
/// bool when(Angel app) {
/// return app.environment == 'production';
/// }
/// ```
bool when(Angel app) => true;
/// Determines if the service provider is ready to be registered with the Angel.
///
/// This method checks two conditions:
/// 1. The service provider is not deferred (checked via the [isDeferred] property).
/// 2. The [when] method returns true for the given [app].
///
/// If both conditions are met, the method returns true, indicating that the
/// service provider is ready to be registered.
///
/// Parameters:
/// - [app]: The [Angel] instance to check against.
///
/// Returns:
/// A boolean value. `true` if the service provider is ready to be registered,
/// `false` otherwise.
bool isReadyToRegister(Angel app) => !isDeferred && when(app);
/// Registers a singleton instance of type [T] with the provided [container].
///
/// This method ensures that the same [instance] of type [T] is always returned
/// when requested from the container. This is useful for objects that should
/// maintain a single state throughout the Angel's lifecycle.
///
/// Parameters:
/// - [container]: The dependency injection container to register with.
/// - [instance]: The instance of type [T] to be registered as a singleton.
///
/// Example:
/// ```dart
/// final myService = MyService();
/// singleton<MyService>(container, myService);
/// ```
///
/// After registration, any request for type [T] from the container will return
/// the same [instance].
void singleton<T>(Container container, T instance) {
container.registerSingleton<T>(instance);
}
/// Registers a factory that always returns the same instance of type [T].
///
/// This method creates a singleton-like behavior using a factory function.
/// The first time the type [T] is requested from the container, the [factory]
/// function is called to create an instance. This instance is then cached and
/// returned for all subsequent requests.
///
/// Parameters:
/// - [container]: The dependency injection container to register with.
/// - [factory]: A function that takes a [Container] and returns an instance of [T].
///
/// Example:
/// ```dart
/// singletonFactory<Database>(container, (c) => Database.connect());
/// ```
///
/// After registration, any request for type [T] from the container will return
/// the same instance, created by the [factory] function on the first request.
void singletonFactory<T>(Container container, T Function(Container) factory) {
T? instance;
container.registerFactory<T>((c) {
instance ??= factory(c);
return instance!;
});
}
/// Registers a factory function for type [T] with the provided [container].
///
/// This method allows you to register a factory function that creates instances
/// of type [T]. The factory function will be called each time an instance of [T]
/// is requested from the container.
///
/// Parameters:
/// - [container]: The dependency injection container to register with.
/// - [factory]: A function that takes a [Container] and returns an instance of [T].
///
/// Example:
/// ```dart
/// bind<Logger>(container, (c) => Logger('MyLogger'));
/// ```
///
/// After registration, you can request an instance of [T] from the container,
/// and the [factory] function will be called to create a new instance each time.
void bind<T>(Container container, T Function(Container) factory) {
container.registerFactory<T>(factory);
}
/// Registers an instance of type [T] with the provided [container].
///
/// This method creates a factory that always returns the same [instance] of type [T].
/// It's useful for registering existing objects into the dependency injection container.
///
/// Parameters:
/// - [container]: The dependency injection container to register with.
/// - [instance]: The instance of type [T] to be registered.
///
/// Example:
/// ```dart
/// final myService = MyService();
/// bindInstance<MyService>(container, myService);
/// ```
///
/// After registration, any request for type [T] from the container will return
/// the same [instance].
void bindInstance<T>(Container container, T instance) {
container.registerFactory<T>((Container c) => instance);
}
/// Registers a factory function for type [T] with the provided [container], but only if a binding for [T] doesn't already exist.
///
/// This method checks if the container already has a binding for type [T]. If it doesn't,
/// it registers the provided [factory] function. This is useful for providing default
/// implementations while allowing them to be overridden.
///
/// Parameters:
/// - [container]: The dependency injection container to register with.
/// - [factory]: A function that takes a [Container] and returns an instance of [T].
///
/// Example:
/// ```dart
/// bindIf<Logger>(container, (c) => Logger('DefaultLogger'));
/// ```
///
/// If [T] is not already registered in the container, the [factory] function will be
/// registered. Otherwise, the existing binding will be preserved.
void bindIf<T>(Container container, T Function(Container) factory) {
if (!container.has<T>()) {
container.registerFactory<T>(factory);
}
}
/// Registers multiple factory bindings at once with the provided container.
///
/// This method allows for batch registration of multiple factory functions
/// for different types in a single call. It iterates through the provided
/// [bindings] map and registers each factory function with its corresponding type.
///
/// Parameters:
/// - [container]: The dependency injection container to register with.
/// - [bindings]: A map where each key is a Type and each value is a factory function.
///
/// Example:
/// ```dart
/// bindMultiple(container, {
/// Logger: (c) => Logger('MyLogger'),
/// Database: (c) => Database.connect(),
/// UserService: (c) => UserService(c.make<Database>())
/// });
/// ```
///
/// Note: The factory functions in the [bindings] map should match the
/// signature `T Function(Container)` where T is the type being registered.
void bindMultiple(Container container, Map<Type, Function> bindings) {
bindings.forEach((type, factory) {
container.registerFactory(factory as Function(Container));
});
}
/// Registers an alias for an existing binding in the container.
///
/// This method creates a new binding of type [A] that resolves to the same
/// instance as an existing binding of type [T]. This is useful when you want
/// to refer to the same service using different types, such as when working
/// with interfaces and implementations.
///
/// Parameters:
/// - [container]: The dependency injection container to register the alias with.
///
/// Type Parameters:
/// - [T]: The original type that is already bound in the container.
/// - [A]: The alias type that will be registered to resolve to the same instance as [T].
///
/// Example:
/// ```dart
/// // Assuming UserService is already registered
/// alias<UserService, IUserService>(container);
/// ```
///
/// After calling this method, requests for type [A] from the container will
/// return the same instance as requests for type [T].
///
/// Note: This method assumes that [T] is already registered in the container
/// and that instances of [T] can be cast to [A]. If these conditions are not met,
/// it may result in runtime errors.
void alias<T, A>(Container container) {
container.registerFactory<A>((c) => c.make<T>() as A);
}
/// Registers a scoped instance of type [T] with the provided [container].
///
/// A scoped instance is created once per scope and reused within that scope.
/// This is useful for objects that should have a single instance per request
/// or transaction, but not shared across the entire Angel.
///
/// Parameters:
/// - [container]: The dependency injection container to register with.
/// - [factory]: A function that takes a [Container] and returns an instance of [T].
///
/// Example:
/// ```dart
/// scoped<RequestContext>(container, (c) => RequestContext());
/// ```
///
/// After registration, a new instance of [T] will be created for each scope,
/// but within the same scope, the same instance will be reused.
void scoped<T>(Container container, T Function(Container) factory) {
container.registerScoped<T>(factory);
}
/// Registers a transient factory for type [T] with the provided [container].
///
/// A transient factory creates a new instance every time it is requested, even within the same scope.
/// This is useful for objects that should not be shared and need a fresh instance each time they are used.
///
/// Parameters:
/// - [container]: The dependency injection container to register with.
/// - [factory]: A function that takes a [Container] and returns a new instance of [T].
///
/// Example:
/// ```dart
/// transient<Logger>(container, (c) => Logger('NewLogger'));
/// ```
///
/// After registration, each request for type [T] from the container will return a new instance
/// created by the [factory] function, regardless of the scope or previous requests.
void transient<T>(Container container, T Function(Container) factory) {
container.registerTransient<T>(factory);
}
/// Registers a constant value of type [T] with the provided [container].
///
/// This method allows you to register a constant value that will always be
/// returned when requesting an instance of type [T] from the container.
/// The registered value cannot be changed after registration.
///
/// Parameters:
/// - [container]: The dependency injection container to register with.
/// - [value]: The constant value of type [T] to be registered.
///
/// Example:
/// ```dart
/// constant<int>(container, 42);
/// constant<String>(container, 'AppName');
/// ```
///
/// After registration, any request for type [T] from the container will
/// always return the registered [value].
void constant<T>(Container container, T value) {
container.registerConstant<T>(value);
}
/// Registers a lazy-loaded singleton of type [T] with the provided [container].
///
/// This method registers a factory function that creates a singleton instance of type [T]
/// only when it is first requested from the container. Subsequent requests will return
/// the same instance.
///
/// Parameters:
/// - [container]: The dependency injection container to register with.
/// - [factory]: A function that takes a [Container] and returns an instance of [T].
///
/// The lazy initialization can help improve performance by deferring the creation
/// of the singleton until it's actually needed.
///
/// Example:
/// ```dart
/// lazySingleton<Database>(container, (c) => Database.connect());
/// ```
///
/// After registration, the first request for type [T] from the container will trigger
/// the [factory] function to create the instance. All subsequent requests will return
/// the same instance without calling the factory again.
void lazySingleton<T>(Container container, T Function(Container) factory) {
container.registerLazySingleton<T>(factory);
}
}
/// A container that manages service providers for dependency injection.
///
/// This class extends the base [Container] and adds functionality to handle
/// [ServiceProvider] instances. It allows for adding, configuring, booting,
/// and discovering service providers.
///
/// The container maintains an internal list of providers, sorted by their priority.
/// It provides methods to add providers manually, configure and boot them with
/// an [Angel] instance, and automatically discover providers using reflection.
///
/// Key features:
/// - Add providers with priority-based sorting
/// - Configure providers with the Angel instance
/// - Boot providers when the Angel starts
/// - Automatically discover and instantiate providers using reflection
///
/// Usage:
/// ```dart
/// var container = ServiceProviderContainer(myReflector);
/// container.addProvider(MyServiceProvider());
/// container.configureProviders(app);
/// container.bootProviders(app);
/// container.discoverProviders();
/// ```
///
/// This container is designed to work with the [ServiceProvider] abstract class
/// and integrates with the Angel framework's [Angel] class.
class ServiceProviderContainer extends Container {
/// Static logger instance for the ServiceProviderContainer class.
///
/// This logger is used throughout the ServiceProviderContainer class to log
/// information, warnings, and errors related to service provider operations.
/// It is initialized with the name 'ServiceProviderContainer' to easily
/// identify log messages originating from this class.
static final Logger _logger = Logger('ServiceProviderContainer');
/// A list to store all registered [ServiceProvider] instances.
///
/// This private field maintains an ordered collection of service providers
/// that have been added to the container. The order of providers in this list
/// is significant, as it determines the sequence in which they are configured
/// and booted.
///
/// Service providers are typically added to this list through the [addProvider]
/// method, which also sorts them based on their priority.
final List<ServiceProvider> _providers = [];
/// Constructs a new [ServiceProviderContainer] instance.
///
/// This constructor initializes a [ServiceProviderContainer] with the provided [reflector].
/// The [reflector] is passed to the superclass constructor and is used for reflection-based
/// operations, such as discovering and instantiating service providers.
///
/// Parameters:
/// - [reflector]: An instance of a reflector that complies with the requirements of the
/// superclass [Container]. This reflector is used for introspection and dynamic
/// instantiation of service providers.
///
/// Example:
/// ```dart
/// var container = ServiceProviderContainer(myReflector);
/// ```
ServiceProviderContainer(super.reflector);
/// Adds a [ServiceProvider] to the container and sorts the providers by priority.
///
/// This method performs the following operations:
/// 1. Adds the given [provider] to the internal list of providers.
/// 2. Sorts the list of providers based on their priority in descending order.
/// 3. Logs the addition of the provider.
///
/// If any exception occurs during this process, it logs the error and rethrows the exception.
///
/// Parameters:
/// - [provider]: The [ServiceProvider] instance to be added to the container.
///
/// Throws:
/// Any exception that occurs during the addition or sorting process.
///
/// Example:
/// ```dart
/// container.addProvider(MyServiceProvider());
/// ```
void addProvider(ServiceProvider provider) {
try {
_providers.add(provider);
_providers.sort((a, b) => b.priority.compareTo(a.priority));
_logger.info('Added provider: ${provider.runtimeType.toString()}');
} catch (e, stackTrace) {
_logger.severe('Error adding provider', e, stackTrace);
rethrow;
}
}
/// Boots all registered service providers that are ready to be registered.
///
/// This method iterates through all registered service providers and calls
/// their `boot` method if they are ready to be registered. It handles the
/// booting process for each provider and logs any errors that occur.
///
/// Parameters:
/// - [app]: The [Angel] instance that providers will use for booting.
///
/// Throws:
/// Any exception that occurs during the booting process of a provider.
/// The exception is logged before being rethrown.
///
/// Note:
/// - Only providers that return true for `isReadyToRegister(app)` are booted.
/// - If an error occurs while booting a provider, it's logged and rethrown,
/// which may interrupt the booting process for subsequent providers.
void bootProviders(Angel app) {
for (var provider in _providers) {
if (provider.isReadyToRegister(app)) {
try {
provider.boot(app);
} catch (e, stackTrace) {
_logger.severe(
'Error booting provider: ${provider.runtimeType}', e, stackTrace);
rethrow;
}
}
}
}
/// Configures all registered service providers that are ready to be registered.
///
/// This method iterates through all registered service providers and calls
/// their `register` method if they are ready to be registered. It handles the
/// configuration process for each provider and logs any errors that occur.
///
/// Parameters:
/// - [app]: The [Angel] instance that providers will use for configuration.
///
/// Throws:
/// Any exception that occurs during the configuration process of a provider.
/// The exception is logged before being rethrown.
///
/// Note:
/// - Only providers that return true for `isReadyToRegister(app)` are configured.
/// - If an error occurs while configuring a provider, it's logged and rethrown,
/// which may interrupt the configuration process for subsequent providers.
void configureProviders(Angel app) {
for (var provider in _providers) {
if (provider.isReadyToRegister(app)) {
try {
provider.register(this, app);
} catch (e, stackTrace) {
_logger.severe('Error configuring provider: ${provider.runtimeType}',
e, stackTrace);
rethrow;
}
}
}
}
/// Discovers and adds service providers to the container using reflection.
///
/// This method iterates through all classes annotated with [discoverableServiceProvider],
/// checks if they are non-abstract subclasses of [ServiceProvider], and if so,
/// instantiates and adds them to the container.
///
/// The discovery process includes:
/// 1. Iterating through annotated classes.
/// 2. Checking if each class is a non-abstract ServiceProvider.
/// 3. Instantiating the provider and adding it to the container.
/// 4. Logging successful additions and any errors encountered.
///
/// If an error occurs during the instantiation of a specific provider,
/// it is logged as a warning, and the method continues with the next provider.
/// If a general error occurs during the discovery process, it is logged
/// as a severe error and rethrown.
///
/// This method is useful for automatically registering service providers
/// without manual instantiation and addition.
///
/// Throws:
/// Any exception that occurs during the general discovery process,
/// excluding individual provider instantiation errors.
void discoverProviders() {
try {
for (var classMirror in discoverableServiceProvider.annotatedClasses) {
if (_isServiceProvider(classMirror) && !classMirror.isAbstract) {
try {
var provider = classMirror.newInstance('', []) as ServiceProvider;
addProvider(provider);
_logger.info(
'Discovered and added provider: ${classMirror.simpleName}');
} catch (e, stackTrace) {
_logger.warning(
'Error instantiating provider: ${classMirror.simpleName}',
e,
stackTrace);
// Continue to the next provider instead of rethrowing
}
}
}
} catch (e, stackTrace) {
_logger.severe('Error discovering providers', e, stackTrace);
rethrow;
}
}
/// Determines if a given ClassMirror represents a subclass of ServiceProvider.
///
/// This method traverses the inheritance hierarchy of the provided [classMirror]
/// to check if it is a subclass of ServiceProvider. It does this by comparing
/// the simple name of each superclass with 'ServiceProvider'.
///
/// Parameters:
/// - [classMirror]: The ClassMirror to check.
///
/// Returns:
/// true if the class is a subclass of ServiceProvider, false otherwise.
///
/// Note:
/// This method assumes that the ServiceProvider class is named exactly
/// 'ServiceProvider'. It will not detect classes that implement an interface
/// named ServiceProvider or classes that are renamed through imports.
bool _isServiceProvider(ClassMirror classMirror) {
ClassMirror? currentMirror = classMirror;
while (currentMirror != null) {
if (currentMirror.simpleName == 'ServiceProvider') {
return true;
}
currentMirror = currentMirror.superclass;
}
return false;
}
}
/// Extension on [Angel] to provide service provider functionality.
extension ServiceProviderExtension on Angel {
/// A static logger instance for the ServiceProviderExtension.
///
/// This logger is used throughout the ServiceProviderExtension to log
/// information, warnings, and errors related to service provider operations.
/// It is initialized with the name 'ServiceProviderExtension' to easily
/// identify log messages originating from this extension.
static final Logger _logger = Logger('ServiceProviderExtension');
/// Adds a [ServiceProvider] to the Angel's container.
///
/// This method attempts to add the given [provider] to the Angel's
/// container. It performs the following steps:
///
/// 1. Checks if the Angel's container is an instance of [ServiceProviderContainer].
/// 2. If it is, adds the provider to the container using the [addProvider] method.
/// 3. If it's not, throws an exception indicating that the container must be a [ServiceProviderContainer].
///
/// If any error occurs during this process, it logs a severe error message
/// and rethrows the exception.
///
/// Parameters:
/// - [provider]: The [ServiceProvider] instance to be added to the container.
///
/// Throws:
/// - [Exception] if the Angel's container is not a [ServiceProviderContainer].
/// - Any other exception that occurs during the process of adding the provider.
///
/// Usage:
/// ```dart
/// app.useServiceProvider(MyServiceProvider());
/// ```
void useServiceProvider(ServiceProvider provider) {
try {
if (container is ServiceProviderContainer) {
(container as ServiceProviderContainer).addProvider(provider);
} else {
throw Exception('Angel container must be a ServiceProviderContainer');
}
} catch (e, stackTrace) {
_logger.severe('Error using service provider', e, stackTrace);
rethrow;
}
}
/// Configures all registered service providers in the Angel's container.
///
/// This method attempts to configure the service providers by calling the
/// `configureProviders` method on the Angel's container. It performs
/// the following steps:
///
/// 1. Checks if the Angel's container is an instance of [ServiceProviderContainer].
/// 2. If it is, calls the `configureProviders` method on the container, passing `this` (the Angel).
/// 3. If it's not, throws an exception indicating that the container must be a [ServiceProviderContainer].
///
/// If any error occurs during this process, it logs a severe error message
/// and rethrows the exception.
///
/// Throws:
/// - [Exception] if the Angel's container is not a [ServiceProviderContainer].
/// - Any other exception that occurs during the configuration process.
///
/// Usage:
/// ```dart
/// app.configureProviders();
/// ```
void configureProviders() {
try {
if (container is ServiceProviderContainer) {
(container as ServiceProviderContainer).configureProviders(this);
} else {
throw Exception('Angel container must be a ServiceProviderContainer');
}
} catch (e, stackTrace) {
_logger.severe('Error configuring providers', e, stackTrace);
rethrow;
}
}
/// Registers and boots all service providers in the Angel's container.
///
/// This method performs the following operations:
/// 1. Checks if the Angel's container is an instance of [ServiceProviderContainer].
/// 2. If it is, it casts the container to [ServiceProviderContainer].
/// 3. Calls [configureProviders] on the container to register the providers.
/// 4. Calls [bootProviders] on the container to initialize the providers.
///
/// If the container is not a [ServiceProviderContainer], it throws an exception.
///
/// This method is typically called after all service providers have been added
/// to the container, either manually or through discovery.
///
/// Throws:
/// - [Exception] if the Angel's container is not a [ServiceProviderContainer].
/// - Any other exception that occurs during the registration or booting process.
/// These exceptions are logged before being rethrown.
///
/// Usage:
/// ```dart
/// app.registerAndBootProviders();
/// ```
void registerAndBootProviders() {
try {
if (container is ServiceProviderContainer) {
var providerContainer = container as ServiceProviderContainer;
providerContainer.configureProviders(this);
providerContainer.bootProviders(this);
} else {
throw Exception('Angel container must be a ServiceProviderContainer');
}
} catch (e, stackTrace) {
_logger.severe('Error registering and booting providers', e, stackTrace);
rethrow;
}
}
/// Discovers, registers, and boots all service providers in the Angel.
///
/// This method performs a complete lifecycle for service providers:
/// 1. Discovers providers using reflection (if the container supports it).
/// 2. Configures the discovered providers.
/// 3. Registers and boots the providers.
///
/// The process includes:
/// - Checking if the Angel's container is a [ServiceProviderContainer].
/// - Calling [discoverProviders] to find and instantiate providers.
/// - Calling [configureProviders] to set up the providers.
/// - Calling [registerAndBootProviders] to finalize provider initialization.
///
/// If the container is not a [ServiceProviderContainer], an exception is thrown.
///
/// This method is typically called once during Angel startup to set up
/// all service providers automatically.
///
/// Throws:
/// - [Exception] if the Angel's container is not a [ServiceProviderContainer].
/// - Any exceptions that occur during the discovery, configuration, or booting process.
/// These exceptions are logged before being rethrown.
///
/// Usage:
/// ```dart
/// app.discoverAndRegisterProviders();
/// ```
void discoverAndRegisterProviders() {
try {
if (container is ServiceProviderContainer) {
(container as ServiceProviderContainer).discoverProviders();
configureProviders();
registerAndBootProviders();
} else {
throw Exception('Angel container must be a ServiceProviderContainer');
}
} catch (e, stackTrace) {
_logger.severe(
'Error discovering and registering providers', e, stackTrace);
rethrow;
}
}
}

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/.dart_tool" />
<excludeFolder url="file://$MODULE_DIR$/.pub" />
<excludeFolder url="file://$MODULE_DIR$/build" />
</content>
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Dart SDK" level="project" />
<orderEntry type="library" name="Dart Packages" level="project" />
</component>
</module>

34
core/support/pubspec.yaml Normal file
View file

@ -0,0 +1,34 @@
name: angel3_support
description: The Support Package for the Protevus Platform
version: 0.0.1
homepage: https://protevus.com
documentation: https://docs.protevus.com
repository: https://github.com/protevus/platformo
environment:
sdk: ^3.4.2
# Add regular dependencies here.
dependencies:
angel3_framework: ^7.0.0
angel3_configuration: ^9.0.0
angel3_container: ^9.0.0
angel3_migration: ^9.0.0
angel3_orm: ^9.0.0
angel3_route: ^9.0.0
angel3_validate: ^9.0.0
collection: ^1.15.0
file: ^7.0.0
get_it: ^7.7.0
injectable: ^2.4.4
logging: ^1.0.0
meta: ^1.3.0
mockito: ^5.4.4
path: ^1.8.0
reflectable: ^4.0.9
dev_dependencies:
build_runner: ^2.3.3
build_test: ^2.1.0
lints: ^3.0.0
test: ^1.24.0

View file

View file

@ -0,0 +1,167 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
import 'package:angel3_container/angel3_container.dart';
import 'package:angel3_framework/angel3_framework.dart';
import 'package:logging/logging.dart';
import 'package:angel3_support/service_provider.dart';
@GenerateMocks([Logger, Container, Angel])
import 'service_provider_test.mocks.dart';
class TestServiceProvider extends ServiceProvider {
@override
void register(Container container, Angel app) {
try {
ServiceProvider.logger.info('Registering TestServiceProvider');
container.registerSingleton<String>('test', as: null);
} catch (e, stackTrace) {
ServiceProvider.logger
.severe('Error registering TestServiceProvider', e, stackTrace);
rethrow;
}
}
@override
Future<void> boot(Angel app) async {
try {
ServiceProvider.logger.info('Booting TestServiceProvider');
await app.configure((_) => null);
} catch (e, stackTrace) {
ServiceProvider.logger
.severe('Error booting TestServiceProvider', e, stackTrace);
rethrow;
}
}
}
void main() {
late MockContainer mockContainer;
late MockAngel mockAngel;
late MockLogger mockLogger;
late TestServiceProvider serviceProvider;
setUp(() {
mockContainer = MockContainer();
mockAngel = MockAngel();
mockLogger = MockLogger();
ServiceProvider.setLogger(mockLogger); // Set the mock logger
serviceProvider = TestServiceProvider();
// Setup default stubs
when(mockContainer.registerSingleton<dynamic>(any, as: anyNamed('as')))
.thenReturn('default');
when(mockContainer.registerFactory<dynamic>(any, as: anyNamed('as')))
.thenReturn((Container c) => 'default');
when(mockAngel.configure(any)).thenAnswer((_) async {});
// Allow any severe logging
when(mockLogger.info(any)).thenReturn(null);
when(mockLogger.severe(any, any, any)).thenReturn(null);
});
test('register method logs info and calls actual registration logic', () {
serviceProvider.register(mockContainer, mockAngel);
verify(mockLogger.info('Registering TestServiceProvider')).called(1);
});
test('boot method logs info and calls actual booting logic', () async {
await serviceProvider.boot(mockAngel);
verify(mockLogger.info('Booting TestServiceProvider')).called(1);
});
test('isDeferred returns false by default', () {
expect(serviceProvider.isDeferred, isFalse);
});
test('provides returns an empty list by default', () {
expect(serviceProvider.provides, isEmpty);
});
test('when returns true by default', () {
expect(serviceProvider.when(mockAngel), isTrue);
});
test(
'isReadyToRegister returns true when not deferred and when condition is met',
() {
expect(serviceProvider.isReadyToRegister(mockAngel), isTrue);
});
test('singleton method registers a singleton in the container', () {
final instance = 'test instance';
serviceProvider.singleton<String>(mockContainer, instance);
verify(mockContainer.registerSingleton<String>(instance, as: null))
.called(1);
});
test('bind method registers a factory in the container', () {
String factory(Container c) => 'test instance';
serviceProvider.bind<String>(mockContainer, factory);
verify(mockContainer.registerFactory<String>(any, as: null)).called(1);
});
test('error in register method is thrown', () {
when(mockContainer.registerSingleton<String>('test', as: null))
.thenThrow(Exception('Test error'));
expect(
() => serviceProvider.register(mockContainer, mockAngel),
throwsException,
);
});
test('error in boot method is thrown', () async {
when(mockAngel.configure(any)).thenThrow(Exception('Test error'));
await expectLater(
() => serviceProvider.boot(mockAngel),
throwsException,
);
});
test('error in register method is logged', () {
when(mockContainer.registerSingleton<String>('test', as: null))
.thenThrow(Exception('Test error'));
expect(
() => serviceProvider.register(mockContainer, mockAngel),
throwsException,
);
final captured =
verify(mockLogger.severe(captureAny, captureAny, captureAny)).captured;
expect(captured[0], 'Error registering TestServiceProvider');
expect(captured[1], isA<Exception>());
expect(captured[2], isA<StackTrace>());
});
test('error in boot method is logged', () async {
when(mockAngel.configure(any)).thenThrow(Exception('Test error'));
await expectLater(
() => serviceProvider.boot(mockAngel),
throwsException,
);
final captured =
verify(mockLogger.severe(captureAny, captureAny, captureAny)).captured;
expect(captured[0], 'Error booting TestServiceProvider');
expect(captured[1], isA<Exception>());
expect(captured[2], isA<StackTrace>());
});
tearDown(() {
// Reset the logger to its original state after each test
ServiceProvider.setLogger(Logger('ServiceProvider'));
});
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff