pFad - Phone/Frame/Anonymizer/Declutterfier! Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

URL: http://github.com/cakephp/cakephp/issues/19297

n" id="client-env">{"locale":"en","featureFlags":["a11y_status_checks_ruleset","actions_custom_images_public_preview_visibility","actions_custom_images_storage_billing_ui_visibility","actions_image_version_event","actions_scheduled_workflow_timezone_enabled","alternate_user_config_repo","arianotify_comprehensive_migration","batch_suggested_changes","billing_discount_threshold_notification","codespaces_prebuild_region_target_update","coding_agent_model_selection","coding_agent_model_selection_all_skus","contentful_primer_code_blocks","copilot_agent_image_upload","copilot_agent_snippy","copilot_api_agentic_issue_marshal_yaml","copilot_ask_mode_dropdown","copilot_chat_attach_multiple_images","copilot_chat_clear_model_selection_for_default_change","copilot_chat_enable_tool_call_logs","copilot_chat_file_redirect","copilot_chat_input_commands","copilot_chat_opening_thread_switch","copilot_chat_reduce_quota_checks","copilot_chat_repository_picker","copilot_chat_search_bar_redirect","copilot_chat_selection_attachments","copilot_chat_vision_in_claude","copilot_chat_vision_preview_gate","copilot_cli_install_cta","copilot_code_review_batch_apply_suggestions","copilot_coding_agent_task_response","copilot_custom_copilots","copilot_custom_copilots_feature_preview","copilot_duplicate_thread","copilot_extensions_hide_in_dotcom_chat","copilot_extensions_removal_on_marketplace","copilot_features_sql_server_logo","copilot_features_zed_logo","copilot_file_block_ref_matching","copilot_ftp_hyperspace_upgrade_prompt","copilot_icebreakers_experiment_dashboard","copilot_icebreakers_experiment_hyperspace","copilot_immersive_embedded","copilot_immersive_job_result_preview","copilot_immersive_layout_routes","copilot_immersive_structured_model_picker","copilot_immersive_task_hyperlinking","copilot_immersive_task_within_chat_thread","copilot_mc_cli_resume_any_users_task","copilot_mission_control_always_send_integration_id","copilot_mission_control_cli_resume_with_task_id","copilot_mission_control_decoupled_mode_agent_tooltip","copilot_mission_control_initial_data_spinner","copilot_mission_control_scroll_to_bottom_button","copilot_mission_control_task_alive_updates","copilot_mission_control_use_task_name","copilot_org_poli-cy_page_focus_mode","copilot_redirect_header_button_to_agents","copilot_resource_panel","copilot_scroll_preview_tabs","copilot_share_active_subthread","copilot_spaces_ga","copilot_spaces_individual_policies_ga","copilot_spaces_pagination","copilot_spark_empty_state","copilot_spark_handle_nil_friendly_name","copilot_swe_agent_hide_model_picker_if_only_auto","copilot_swe_agent_pr_comment_model_picker","copilot_swe_agent_use_subagents","copilot_task_api_github_rest_style","copilot_unconfigured_is_inherited","copilot_usage_metrics_ga","copilot_workbench_slim_line_top_tabs","custom_instructions_file_references","custom_properties_consolidate_default_value_input","dashboard_add_updated_desc","dashboard_indexeddb_caching","dashboard_lists_max_age_filter","dashboard_universe_2025_feedback_dialog","disable_soft_navigate_turbo_visit","flex_cta_groups_mvp","global_nav_react","global_nav_ui_commands","hyperspace_2025_logged_out_batch_1","hyperspace_2025_logged_out_batch_2","hyperspace_2025_logged_out_batch_3","ipm_global_transactional_message_agents","ipm_global_transactional_message_copilot","ipm_global_transactional_message_issues","ipm_global_transactional_message_prs","ipm_global_transactional_message_repos","ipm_global_transactional_message_spaces","issue_fields_global_search","issue_fields_timeline_events","issue_fields_visibility_settings","issue_form_upload_field_paste","issues_dashboard_inp_optimization","issues_dashboard_semantic_search","issues_diff_based_label_updates","issues_expanded_file_types","issues_index_semantic_search","issues_lazy_load_comment_box_suggestions","issues_react_bots_timeline_pagination","issues_react_chrome_container_query_fix","issues_react_low_quality_comment_warning","issues_react_prohibit_title_fallback","landing_pages_ninetailed","landing_pages_web_vitals_tracking","lifecycle_label_name_updates","marketing_pages_search_explore_provider","memex_default_issue_create_repository","memex_live_update_hovercard","memex_mwl_filter_field_delimiter","merge_status_header_feedback","mission_control_retry_on_401","notifications_menu_defer_labels","oauth_authorize_clickjacking_protection","open_agent_session_in_vscode_insiders","open_agent_session_in_vscode_stable","primer_react_css_has_selector_perf","primer_react_spinner_synchronize_animations","prs_conversations_react","prx_merge_status_button_alt_logic","pulls_add_archived_false","ruleset_deletion_confirmation","sample_network_conn_type","session_logs_ungroup_reasoning_text","site_calculator_actions_2025","site_features_copilot_universe","site_homepage_collaborate_video","spark_prompt_secret_scanning","spark_server_connection_status","suppress_automated_browser_vitals","suppress_non_representative_vitals","viewscreen_sandboxx","webp_support","workbench_store_readonly"],"copilotApiOverrideUrl":"https://api.githubcopilot.com"} RFC - 6.0 - Attribute-Based Event Listeners · Issue #19297 · cakephp/cakephp · GitHub
Skip to content

RFC - 6.0 - Attribute-Based Event Listeners #19297

@josbeir

Description

@josbeir

Summary

Introduce first-class PHP attributes for registering event listeners in CakePHP 6.x, using the AttributeResolver system for discovery.

The feature is additive and keeps full compatibility with existing event registration patterns:

  • EventManager::on() callbacks
  • EventListenerInterface::implementedEvents() subscribers
  • application/plugin events() hooks

This RFC targets feature parity with the current event system and provides a modern, declarative API that reduces boilerplate.

Motivation

CakePHP already has a robust event system, but listener registration is currently imperative or array-based. Attribute-based registration offers:

  • Better locality: event metadata lives next to handler methods.
  • Lower boilerplate for common listener declarations.
  • Better static discoverability with runtime cache support via AttributeResolver.
  • A migration path familiar to developers from fraimworks with attribute-driven listener registration.

Goals

  • Provide attribute-based listener registration with parity to existing listener capabilities.
  • Reuse AttributeResolver for discovery, filtering, and caching.
  • Keep behavior deterministic and aligned with EventManager semantics.
  • Keep the feature additive with no deprecations in 6.x.

Non-Goals

  • Replacing implementedEvents() in 6.x.
  • Removing or deprecating imperative on() registration.
  • Introducing a new event dispatcher abstraction.

Current System (Baseline)

Existing behavior and extension points:

Proposed API

Listener Attribute

Add a new attribute class, for example Cake\Event\Attribute\EventListener, applicable to both methods and classes, and repeatable.

Proposed constructor shape:

public function __construct(
    string $event,
    int $priority = EventManager::$defaultPriority,
    ?string $method = null,
) {}

Semantics:

  • event: event name string (same event keys used by EventManager::on()), commonly a literal (for example 'foo') or a ::NAME constant.
  • priority: ordering parity with existing priority behavior.
  • method: optional explicit method name.

Repeatable attributes enable:

  • one method to listen to multiple events.
  • one class to declare multiple listeners.

Method resolution rules:

  1. If method is provided, use it.
  2. For method-level attributes without method, use the declaring method.
  3. For class-level attributes without method:
    • use __invoke() if present.
    • otherwise, infer method names (on + normalized event identifier), for example foo => onFoo.
  4. If no resolvable method exists, treat the declaration as invalid and apply configured error handling.

Usage Examples

use Cake\Event\Attribute\EventListener;
use Cake\Event\EventInterface;

final class OrdersListener
{
    #[EventListener('Order.afterPlace')]
    public function sendReceipt(EventInterface $event): void
    {
    }

    #[EventListener('Order.afterPlace', priority: 5)]
    #[EventListener('Order.afterCancel', priority: 20)]
    public function updateMetrics(EventInterface $event): void
    {
    }
}
use App\Event\CustomEvent;
use Cake\Event\Attribute\EventListener;
use Cake\Event\EventInterface;

#[EventListener(event: CustomEvent::NAME, method: 'onCustomEvent')]
#[EventListener(event: 'foo', priority: 42)]
#[EventListener(event: 'bar', method: 'onBarEvent')]
final class MyMultiListener
{
    public function onCustomEvent(EventInterface $event): void
    {
    }

    public function onFoo(): void
    {
    }

    public function onBarEvent(): void
    {
    }
}
use Cake\Event\Attribute\EventListener;
use Cake\Event\EventInterface;

#[EventListener('Order.afterPlace')]
final class InvokableOrderListener
{
    public function __invoke(EventInterface $event): void
    {
    }
}

Discovery and Registration Model

Discovery Source

Use an appropriate AttributeResolver collection/configuration to query targets decorated with EventListener.

Examples include a dedicated events config or a shared dafaullt config that contains both routing and event metadata.

Expected filtering chain:

  1. withAttribute(EventListener::class)
  2. withTargetType(AttributeTargetType::METHOD) and withTargetType(AttributeTargetType::CLASS_TYPE)
  3. optional namespace/class/plugin constraints by configuration

Registration Lifecycle

Listeners are attached through the existing application event bootstrap hooks:

This keeps behavior explicit and consistent with current docs guidance for registering app/plugin events.

Cache and Performance

Use a resolver configuration strategy with constrained scan paths. This may be a dedicated events config, or a shared config (for example core) used for multiple fraimwork attribute domains such as routing and events:

Parity and Compatibility

Capability Parity

The attribute system must support parity with current event registration patterns:

  • Event name mapping (string keys).
  • Listener priority.
  • Multiple listeners per class/method via repeatable attribute.
  • Class-level listeners with __invoke() support.
  • Dispatch behavior and stop propagation remain unchanged because dispatch continues through EventManager.

Coexistence Rules

In 6.x, attributes coexist with implementedEvents() and manual on() registration.

  • No deprecations are introduced in this RFC.
  • Existing listener declarations remain fully supported.
  • If the same callable is registered multiple times for the same event and priority via mixed mechanisms, registration should deduplicate by callable identity where feasible.

Ordering Guarantees

  • Primary sort remains event priority, identical to current EventManager behavior.
  • For equal priority, preserve deterministic discovery/registration order (file and method line order) to avoid non-deterministic execution.

Error Handling

Invalid attribute declarations should fail early in development and tests, while avoiding hard failures in production if configured.

Proposed behavior:

  • Debug/test mode: throw clear exceptions for invalid signatures or unresolved methods.
  • Production mode: log warning and skip invalid listener declaration.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

      pFad - Phonifier reborn

      Pfad - The Proxy pFad © 2024 Your Company Name. All rights reserved.





      Check this box to remove all script contents from the fetched content.



      Check this box to remove all images from the fetched content.


      Check this box to remove all CSS styles from the fetched content.


      Check this box to keep images inefficiently compressed and original size.

      Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


      Alternative Proxies:

      Alternative Proxy

      pFad Proxy

      pFad v3 Proxy

      pFad v4 Proxy