namespace, '/organizers', [ [ 'methods' => 'GET', 'callback' => [ $this, 'get_organizers' ], 'permission_callback' => [ $this, 'get_organizers_permissions_check' ], ], [ 'methods' => 'POST', 'callback' => [ $this, 'create_organizer' ], 'permission_callback' => [ $this, 'manage_organizers_permissions_check' ], 'args' => $this->organizer_args( true ), ], ] ); register_rest_route( $this->namespace, '/organizers/(?P\d+)', [ [ 'methods' => 'GET', 'callback' => [ $this, 'get_organizer' ], // The full record (address, contact details) is the // management screen's view; only the id/name list is public. 'permission_callback' => [ $this, 'manage_organizers_permissions_check' ], ], [ 'methods' => 'PUT, PATCH', 'callback' => [ $this, 'update_organizer' ], 'permission_callback' => [ $this, 'manage_organizers_permissions_check' ], 'args' => $this->organizer_args( false ), ], [ 'methods' => 'DELETE', 'callback' => [ $this, 'delete_organizer' ], 'permission_callback' => [ $this, 'manage_organizers_permissions_check' ], ], ] ); } /** * Args shared by create and update. * * @param bool $creating Whether a name is required. * @return array> * * @psalm-suppress UnusedMethod -- Invoked during register_routes(). */ private function organizer_args( bool $creating ): array { return [ 'name' => $this->rest_arg_string( 100, $creating ), 'email' => $this->rest_arg_string( 100, false ), 'phone' => $this->rest_arg_string( 40, false ), 'url' => $this->rest_arg_url( false, true ), ]; } /** * Creating, renaming and removing organizers is a management action. * * @psalm-suppress PossiblyUnusedMethod -- Called by the WP REST API as a permission callback. */ public function manage_organizers_permissions_check( WP_REST_Request $request ): bool|\WP_Error { $rate_limit = $this->check_rate_limit(); if ( is_wp_error( $rate_limit ) ) { return $rate_limit; } return $this->check_permission( 'webcal_can_manage_categories' ); } /** * Anyone who may view the calendar may see who runs its events. */ public function get_organizers_permissions_check( WP_REST_Request $request ): bool|\WP_Error { $rate_limit = $this->check_rate_limit(); if ( is_wp_error( $rate_limit ) ) { return $rate_limit; } // Public read, matching GET /events: the Filter Bar on a public page // needs this list for logged-out visitors, and it hides its controls // when the list is empty — so a 403 here removed the feature entirely // from the calendars it was built for. Only ids and names are // exposed; addresses, coordinates and contact details are not. if ( ! is_user_logged_in() ) { return true; } return $this->check_permission( 'webcal_can_view' ); } /** * List every organizer as id + name. * * @psalm-suppress PossiblyUnusedParam -- The REST framework always passes $request. */ public function get_organizers( WP_REST_Request $request ): WP_REST_Response|WP_Error { try { $repo = $this->container->get_organizer_repository(); $organizers = $repo->findAll(); // Counts for managers only — see the note in VenuesController. $counts = current_user_can( 'webcal_can_manage_categories' ); return new WP_REST_Response( array_values( array_map( static function ( $organizer ) use ( $counts, $repo ): array { $row = [ 'id' => $organizer->id()->value(), 'name' => $organizer->name(), ]; if ( $counts ) { $row['event_count'] = $repo->countEvents( $organizer->id() ); } return $row; }, $organizers ) ), 200 ); } catch ( \Exception $e ) { $this->get_logger()->error( 'REST Error in OrganizersController (get_organizers): ' . esc_html( $e->getMessage() ) ); return new WP_Error( 'webcal_error', $e->getMessage(), [ 'status' => 500 ] ); } } /** * One organizer, in full. * * @psalm-suppress PossiblyUnusedMethod -- Called by the WP REST API as a route callback. * @psalm-suppress PossiblyUnusedParam -- The REST framework always passes $request. */ public function get_organizer( WP_REST_Request $request ): WP_REST_Response|WP_Error { try { $organizer = $this->container->get_organizer_repository()->findById( new \WebCalendar\Core\Domain\ValueObject\OrganizerId( $this->int( $request->get_param( 'id' ) ) ) ); if ( null === $organizer ) { return new WP_Error( 'webcal_not_found', __( 'Organizer not found', 'agenticdaisy-calendar' ), [ 'status' => 404 ] ); } return new WP_REST_Response( self::to_array( $organizer ), 200 ); } catch ( \Exception $e ) { $this->get_logger()->error( 'REST Error in OrganizersController (get_organizer): ' . esc_html( $e->getMessage() ) ); return new WP_Error( 'webcal_error', $e->getMessage(), [ 'status' => 500 ] ); } } /** * Create an organizer. * * @psalm-suppress PossiblyUnusedMethod -- Called by the WP REST API as a route callback. * @psalm-suppress PossiblyUnusedParam -- The REST framework always passes $request. */ public function create_organizer( WP_REST_Request $request ): WP_REST_Response|WP_Error { $name = trim( $this->str( $request->get_param( 'name' ) ) ); if ( '' === $name ) { return new WP_Error( 'rest_invalid_param', __( 'An organizer needs a name.', 'agenticdaisy-calendar' ), [ 'status' => 400 ] ); } try { $repo = $this->container->get_organizer_repository(); $organizer = new \WebCalendar\Core\Domain\Entity\Organizer( // Id 0 means "new" — see the note in VenuesController. id: new \WebCalendar\Core\Domain\ValueObject\OrganizerId( 0 ), name: $name, email: $this->nullable( $request->get_param( 'email' ) ), phone: $this->nullable( $request->get_param( 'phone' ) ), url: $this->nullable( $request->get_param( 'url' ) ), ); return new WP_REST_Response( self::to_array( $repo->save( $organizer ) ), 201 ); } catch ( \InvalidArgumentException $e ) { return new WP_Error( 'rest_invalid_param', $e->getMessage(), [ 'status' => 400 ] ); } catch ( \Exception $e ) { $this->get_logger()->error( 'REST Error in OrganizersController (create_organizer): ' . esc_html( $e->getMessage() ) ); return new WP_Error( 'webcal_error', $e->getMessage(), [ 'status' => 500 ] ); } } /** * Update an organizer, leaving out what the request did not mention. * * @psalm-suppress PossiblyUnusedMethod -- Called by the WP REST API as a route callback. * @psalm-suppress PossiblyUnusedParam -- The REST framework always passes $request. */ public function update_organizer( WP_REST_Request $request ): WP_REST_Response|WP_Error { try { $repo = $this->container->get_organizer_repository(); $organizer_id = new \WebCalendar\Core\Domain\ValueObject\OrganizerId( $this->int( $request->get_param( 'id' ) ) ); $existing = $repo->findById( $organizer_id ); if ( null === $existing ) { return new WP_Error( 'webcal_not_found', __( 'Organizer not found', 'agenticdaisy-calendar' ), [ 'status' => 404 ] ); } $name = null === $request->get_param( 'name' ) ? $existing->name() : trim( $this->str( $request->get_param( 'name' ) ) ); if ( '' === $name ) { return new WP_Error( 'rest_invalid_param', __( 'An organizer needs a name.', 'agenticdaisy-calendar' ), [ 'status' => 400 ] ); } $organizer = new \WebCalendar\Core\Domain\Entity\Organizer( id: $organizer_id, name: $name, email: $this->kept( $request, 'email', $existing->email() ), phone: $this->kept( $request, 'phone', $existing->phone() ), url: $this->kept( $request, 'url', $existing->url() ), ); return new WP_REST_Response( self::to_array( $repo->save( $organizer ) ), 200 ); } catch ( \InvalidArgumentException $e ) { return new WP_Error( 'rest_invalid_param', $e->getMessage(), [ 'status' => 400 ] ); } catch ( \Exception $e ) { $this->get_logger()->error( 'REST Error in OrganizersController (update_organizer): ' . esc_html( $e->getMessage() ) ); return new WP_Error( 'webcal_error', $e->getMessage(), [ 'status' => 500 ] ); } } /** * Delete an organizer, optionally moving its events to another one. * * As with venues, events left pointing at the deleted row render as * having no organizer rather than blocking the delete. * * @psalm-suppress PossiblyUnusedMethod -- Called by the WP REST API as a route callback. * @psalm-suppress PossiblyUnusedParam -- The REST framework always passes $request. */ public function delete_organizer( WP_REST_Request $request ): WP_REST_Response|WP_Error { try { $repo = $this->container->get_organizer_repository(); $organizer_id = new \WebCalendar\Core\Domain\ValueObject\OrganizerId( $this->int( $request->get_param( 'id' ) ) ); if ( null === $repo->findById( $organizer_id ) ) { return new WP_Error( 'webcal_not_found', __( 'Organizer not found', 'agenticdaisy-calendar' ), [ 'status' => 404 ] ); } $reassign_raw = $request->get_param( 'reassign_to' ); $reassign = $this->int( $reassign_raw ); if ( $reassign > 0 ) { if ( $reassign === $organizer_id->value() ) { return new WP_Error( 'rest_invalid_param', __( 'An organizer cannot be reassigned to itself.', 'agenticdaisy-calendar' ), [ 'status' => 400 ] ); } $target = new \WebCalendar\Core\Domain\ValueObject\OrganizerId( $reassign ); if ( null === $repo->findById( $target ) ) { return new WP_Error( 'rest_invalid_param', __( 'No organizer with that id exists.', 'agenticdaisy-calendar' ), [ 'status' => 400 ] ); } $repo->reassignEvents( $organizer_id, $target ); } $repo->delete( $organizer_id ); return new WP_REST_Response( [ 'deleted' => true ], 200 ); } catch ( \Exception $e ) { $this->get_logger()->error( 'REST Error in OrganizersController (delete_organizer): ' . esc_html( $e->getMessage() ) ); return new WP_Error( 'webcal_error', $e->getMessage(), [ 'status' => 500 ] ); } } /** * The full organizer record, as the event payload also renders it. * * @return array * * @psalm-suppress UnusedMethod -- Called from every route callback in this class. */ private static function to_array( \WebCalendar\Core\Domain\Entity\Organizer $organizer ): array { return [ 'id' => $organizer->id()->value(), 'name' => $organizer->name(), 'email' => $organizer->email(), 'phone' => $organizer->phone(), 'url' => $organizer->url(), ]; } }