*/ private $services = []; /** * Get the container instance. * * @return Container */ public static function get_instance(): self { if ( null === self::$instance ) { self::$instance = new self(); } return self::$instance; } /** * Replace the singleton instance (for testing only). * * @param self|null $instance The instance to inject, or null to reset. */ public static function set_test_instance( ?self $instance ): void { self::$instance = $instance; } /** * Override a single service entry (for testing only). * * @param string $id Service ID. * @param mixed $service The service instance to inject. */ public function set_service( string $id, mixed $service ): void { $this->services[ $id ] = $service; } /** * Get a service. * * @param string $id Service ID or class string. * @return mixed */ public function get( string $id ): mixed { if ( ! isset( $this->services[ $id ] ) ) { $this->services[ $id ] = $this->create( $id ); } return $this->services[ $id ]; } // -- Typed accessors: one per registered service. Provides IDE // -- autocompletion and PHPStan return-type safety that a generic // -- get() call cannot offer. Each delegates to the lazy-loading // -- get($id) method above. public function get_logger(): \WebCalendarDeps\Psr\Log\LoggerInterface { /** @var \WebCalendarDeps\Psr\Log\LoggerInterface */ return $this->get( 'logger' ); } public function get_pdo(): \PDO { /** @var \PDO */ return $this->get( 'pdo' ); } public function get_user_repository(): \WebCalendar\Core\Domain\Repository\UserRepositoryInterface { /** @var \WebCalendar\Core\Domain\Repository\UserRepositoryInterface */ return $this->get( 'user_repository' ); } public function get_participant_repository(): \WebCalendar\WP\Bridge\WpParticipantRepository { /** @var \WebCalendar\WP\Bridge\WpParticipantRepository */ return $this->get( 'participant_repository' ); } public function get_reminder_repository(): \WebCalendar\WP\Bridge\WpReminderRepository { /** @var \WebCalendar\WP\Bridge\WpReminderRepository */ return $this->get( 'reminder_repository' ); } /** * The core repository itself, not the WpReminderRepository bridge. * * The bridge exposes only the two methods ReminderProcessor needs and * does not implement ReminderRepositoryInterface, so core services that * do take the interface — EventDuplicationService — need this instead. */ public function get_core_reminder_repository(): \WebCalendar\Core\Domain\Repository\ReminderRepositoryInterface { /** @var \WebCalendar\Core\Domain\Repository\ReminderRepositoryInterface */ return $this->get( 'core_reminder_repository' ); } public function get_resource_repository(): \WebCalendar\Core\Domain\Repository\ResourceRepositoryInterface { /** @var \WebCalendar\Core\Domain\Repository\ResourceRepositoryInterface */ return $this->get( 'resource_repository' ); } public function get_email_template(): \WebCalendar\WP\Bridge\WpEmailTemplate { /** @var \WebCalendar\WP\Bridge\WpEmailTemplate */ return $this->get( 'email_template' ); } public function get_auth_service(): \WebCalendar\WP\Bridge\WpAuthService { /** @var \WebCalendar\WP\Bridge\WpAuthService */ return $this->get( 'auth_service' ); } public function get_permission_repository(): \WebCalendar\Core\Domain\Repository\PermissionRepositoryInterface { /** @var \WebCalendar\Core\Domain\Repository\PermissionRepositoryInterface */ return $this->get( 'permission_repository' ); } public function get_config_repository(): \WebCalendar\Core\Domain\Repository\ConfigRepositoryInterface { /** @var \WebCalendar\Core\Domain\Repository\ConfigRepositoryInterface */ return $this->get( 'config_repository' ); } public function get_license(): \WebCalendar\WP\License\License { /** @var \WebCalendar\WP\License\License */ return $this->get( 'license' ); } public function get_event_repository(): \WebCalendar\Core\Domain\Repository\EventRepositoryInterface { /** @var \WebCalendar\Core\Domain\Repository\EventRepositoryInterface */ return $this->get( 'event_repository' ); } public function get_category_repository(): \WebCalendar\Core\Domain\Repository\CategoryRepositoryInterface { /** @var \WebCalendar\Core\Domain\Repository\CategoryRepositoryInterface */ return $this->get( 'category_repository' ); } public function get_event_service(): \WebCalendar\Core\Application\Service\EventService { /** @var \WebCalendar\Core\Application\Service\EventService */ return $this->get( 'event_service' ); } public function get_event_duplication_service(): \WebCalendar\Core\Application\Service\EventDuplicationService { /** @var \WebCalendar\Core\Application\Service\EventDuplicationService */ return $this->get( 'event_duplication_service' ); } public function get_permission_service(): \WebCalendar\Core\Application\Service\PermissionService { /** @var \WebCalendar\Core\Application\Service\PermissionService */ return $this->get( 'permission_service' ); } public function get_category_service(): \WebCalendar\Core\Application\Service\CategoryService { /** @var \WebCalendar\Core\Application\Service\CategoryService */ return $this->get( 'category_service' ); } public function get_recurrence_service(): \WebCalendar\Core\Application\Service\RecurrenceService { /** @var \WebCalendar\Core\Application\Service\RecurrenceService */ return $this->get( 'recurrence_service' ); } public function get_email_provider(): \WebCalendar\Core\Application\Contract\EmailProviderInterface { /** @var \WebCalendar\Core\Application\Contract\EmailProviderInterface */ return $this->get( 'email_provider' ); } public function get_webhook_provider(): \WebCalendar\Core\Application\Contract\WebhookProviderInterface { /** @var \WebCalendar\Core\Application\Contract\WebhookProviderInterface */ return $this->get( 'webhook_provider' ); } public function get_notification_service(): \WebCalendar\Core\Application\Service\NotificationService { /** @var \WebCalendar\Core\Application\Service\NotificationService */ return $this->get( 'notification_service' ); } public function get_email_queue(): \WebCalendar\WP\EmailQueue { /** @var \WebCalendar\WP\EmailQueue */ return $this->get( 'email_queue' ); } public function get_reminder_processor(): \WebCalendar\WP\ReminderProcessor { /** @var \WebCalendar\WP\ReminderProcessor */ return $this->get( 'reminder_processor' ); } public function get_notification_manager(): \WebCalendar\WP\NotificationManager { /** @var \WebCalendar\WP\NotificationManager */ return $this->get( 'notification_manager' ); } public function get_rate_limiter(): \WebCalendar\WP\REST\RateLimiter { /** @var \WebCalendar\WP\REST\RateLimiter */ return $this->get( 'rate_limiter' ); } public function get_event_transformer(): \WebCalendar\WP\REST\EventTransformer { /** @var \WebCalendar\WP\REST\EventTransformer */ return $this->get( 'event_transformer' ); } public function get_event_action_service(): \WebCalendar\WP\EventActionService { /** @var \WebCalendar\WP\EventActionService */ return $this->get( 'event_action_service' ); } public function get_event_mapper(): \WebCalendar\Core\Infrastructure\ICal\EventMapper { /** @var \WebCalendar\Core\Infrastructure\ICal\EventMapper */ return $this->get( 'event_mapper' ); } public function get_import_service(): \WebCalendar\Core\Application\Service\ImportService { /** @var \WebCalendar\Core\Application\Service\ImportService */ return $this->get( 'import_service' ); } public function get_export_service(): \WebCalendar\Core\Application\Service\ExportService { /** @var \WebCalendar\Core\Application\Service\ExportService */ return $this->get( 'export_service' ); } public function get_remote_calendar_service(): \WebCalendar\WP\RemoteCalendarService { /** @var \WebCalendar\WP\RemoteCalendarService */ return $this->get( 'remote_calendar_service' ); } public function get_conflict_service(): \WebCalendar\WP\ConflictService { /** @var \WebCalendar\WP\ConflictService */ return $this->get( 'conflict_service' ); } public function get_holiday_provider_registry(): HolidayProviderRegistry { /** @var HolidayProviderRegistry */ return $this->get( 'holiday_provider_registry' ); } public function get_ticket_repository(): \WebCalendar\Core\Domain\Repository\TicketRepositoryInterface { /** @var \WebCalendar\Core\Domain\Repository\TicketRepositoryInterface */ return $this->get( 'ticket_repository' ); } public function get_ticket_service(): \WebCalendar\Core\Application\Service\TicketService { /** @var \WebCalendar\Core\Application\Service\TicketService */ return $this->get( 'ticket_service' ); } public function get_pending_rsvp_repository(): \WebCalendar\WP\Rsvp\PendingRsvpRepository { /** @var \WebCalendar\WP\Rsvp\PendingRsvpRepository */ return $this->get( 'pending_rsvp_repository' ); } public function get_rsvp_mailer(): \WebCalendar\WP\Rsvp\RsvpMailer { /** @var \WebCalendar\WP\Rsvp\RsvpMailer */ return $this->get( 'rsvp_mailer' ); } public function get_rsvp_service(): \WebCalendar\WP\Rsvp\RsvpService { /** @var \WebCalendar\WP\Rsvp\RsvpService */ return $this->get( 'rsvp_service' ); } /** * Magic method to allow typed service retrieval. * * @param string $name Method name. * @param array $arguments Arguments. * @return mixed */ public function __call( string $name, array $arguments ): mixed { if ( str_starts_with( $name, 'get_' ) ) { $id = substr( $name, 4 ); return $this->get( $id ); } // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- exception message is not rendered as HTML throw new \BadMethodCallException( "Method $name does not exist." ); } /** * Create a service. * * @param string $id Service ID. * @return mixed */ private function create( string $id ): mixed { global $wpdb; switch ( $id ) { case 'logger': return new WpLogger(); case 'pdo': return PdoFactory::create(); case 'user_repository': return new WpUserRepository(); case 'participant_repository': return new WpParticipantRepository( $this->get_event_repository(), $this->get_resource_repository() ); case 'core_reminder_repository': /** @var \PDO $pdo */ $pdo = $this->get_pdo(); return new PdoReminderRepository( $pdo, $wpdb->prefix ); case 'reminder_repository': return new WpReminderRepository( $this->get_core_reminder_repository() ); case 'resource_repository': /** @var \PDO $pdo */ $pdo = $this->get_pdo(); return new PdoResourceRepository( $pdo, $wpdb->prefix ); case 'email_template': return new WpEmailTemplate(); case 'auth_service': return new WpAuthService(); case 'permission_repository': return new WpPermissionRepository(); case 'config_repository': return new WpConfigRepository(); case 'license': return new License(); case 'event_repository': /** @var \PDO $pdo */ $pdo = $this->get_pdo(); return new PdoEventRepository( $pdo, $wpdb->prefix ); case 'category_repository': /** @var \PDO $pdo */ $pdo = $this->get_pdo(); return new PdoCategoryRepository( $pdo, $wpdb->prefix ); case 'venue_repository': $pdo = $this->get_pdo(); return new \WebCalendar\Core\Infrastructure\Persistence\PdoVenueRepository( $pdo, $wpdb->prefix ); case 'organizer_repository': $pdo = $this->get_pdo(); return new \WebCalendar\Core\Infrastructure\Persistence\PdoOrganizerRepository( $pdo, $wpdb->prefix ); case 'site_extra_repository': $pdo = $this->get_pdo(); return new \WebCalendar\Core\Infrastructure\Persistence\PdoSiteExtraRepository( $pdo, $wpdb->prefix ); case 'ticket_repository': $pdo = $this->get_pdo(); return new \WebCalendar\Core\Infrastructure\Persistence\PdoTicketRepository( $pdo, $wpdb->prefix ); case 'ticket_service': // The payment provider argument stays null on purpose. A // price of 0 is an RSVP, and TicketService::purchase() only // reaches PaymentProviderInterface when the type is not // free, so the RSVP path never needs one. Passing a provider // here would be the first step towards paid ticketing, which // is off the roadmap by an explicit decision. return new \WebCalendar\Core\Application\Service\TicketService( $this->get_ticket_repository(), null, $this->get_activity_log_service(), $this->get_logger() ); case 'pending_rsvp_repository': return new \WebCalendar\WP\Rsvp\PendingRsvpRepository(); case 'rsvp_mailer': return new \WebCalendar\WP\Rsvp\RsvpMailer( $this->get_email_queue() ); case 'rsvp_service': return new \WebCalendar\WP\Rsvp\RsvpService( $this->get_ticket_service(), $this->get_ticket_repository(), $this->get_event_repository(), $this->get_pending_rsvp_repository(), $this->get_rsvp_mailer(), $this->get_logger() ); case 'site_extra_service': return new \WebCalendar\Core\Application\Service\SiteExtraService( $this->get_site_extra_repository() ); case 'search_service': return new \WebCalendar\Core\Application\Service\SearchService( $this->get_event_repository(), $this->get_logger() ); case 'event_service': return new EventService( $this->get_event_repository(), $this->get_user_repository(), $this->get_logger() ); case 'event_duplication_service': // The site-extra repository was null here for a release, // justified by a comment saying the plugin had no // custom-fields surface. Once it grew one, the null quietly // meant every "Duplicate" produced a copy missing its // custom-field values — the service is built to copy them // and was never handed the means. return new EventDuplicationService( $this->get_event_repository(), $this->get_category_repository(), $this->get_core_reminder_repository(), $this->get_site_extra_repository(), $this->get_activity_log_service(), $this->get_logger() ); case 'permission_service': return new PermissionService( $this->get_permission_repository() ); case 'category_service': // The event repository lets core check that a non-admin // assigning categories created or attends the event. return new CategoryService( $this->get_category_repository(), $this->get_logger(), $this->get_event_repository() ); case 'recurrence_service': return new RecurrenceService(); case 'email_provider': return new WpEmailProvider(); case 'webhook_provider': return new WpWebhookProvider(); case 'notification_service': return new NotificationService( $this->get_email_provider(), $this->get_webhook_provider(), $this->get_logger() ); case 'email_queue': return new EmailQueue( $this->get_logger() ); case 'reminder_processor': return new ReminderProcessor( $this->get_logger() ); case 'notification_manager': return new NotificationManager(); case 'rate_limiter': return new RateLimiter(); case 'event_transformer': return new EventTransformer(); case 'event_action_service': return new EventActionService(); case 'conflict_service': return new ConflictService( new \WebCalendar\Core\Domain\Service\ConflictDetector(), $this->get_event_repository(), $this->get_config_repository() ); case 'event_mapper': // Pass the site timezone so the importer rebases absolute (UTC "Z" // or zoned "TZID") date-times to local wall-clock before they are // stored in the timezone-naive webcal tables. Without this, UTC // times are stored verbatim and appear shifted by the UTC offset // (e.g. 1 hour during BST). return new EventMapper( wp_timezone() ); case 'import_service': $raw_max = get_option( 'webcal_import_max_events', 5000 ); $opt_max = is_scalar( $raw_max ) ? (int) $raw_max : 5000; /** @var int $max_events */ $max_events = apply_filters( 'webcal_import_max_events', $opt_max ); return new ImportService( eventRepository: $this->get_event_repository(), eventMapper: $this->get_event_mapper(), categoryRepository: $this->get_category_repository(), maxEvents: max( 1, $max_events ), logger: $this->get_logger() ); case 'export_service': return new ExportService( $this->get_event_mapper() ); case 'layer_repository': /** @var \PDO $pdo */ $pdo = $this->get_pdo(); return new PdoLayerRepository( $pdo, $wpdb->prefix ); case 'layer_service': return new LayerService( $this->get_layer_repository() ); case 'resource_service': return new ResourceService( $this->get_resource_repository() ); case 'holiday_provider_registry': return new HolidayProviderRegistry( new NagerDateProvider(), new GoogleCalendarProvider(), new ThunderbirdProvider() ); case 'activity_log_repository': $pdo = $this->get_pdo(); return new PdoActivityLogRepository( $pdo, $wpdb->prefix ); case 'activity_log_service': /** @var \WebCalendar\Core\Domain\Repository\ActivityLogRepositoryInterface $repo */ $repo = $this->get( 'activity_log_repository' ); return new ActivityLogService( $repo ); case 'remote_calendar_service': return new RemoteCalendarService( $this->get_resource_repository(), $this->get_event_repository(), $this->get_import_service(), $this->get_logger() ); default: // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- exception message is not rendered as HTML throw new \InvalidArgumentException( "Unknown service: $id" ); } } }