Skip to content

Conversation

lonnieezell
Copy link
Member

@lonnieezell lonnieezell commented Oct 5, 2025

Description
This adds the ability to use attributes on controller classes or methods. The primary goal of this is to get us as close to feature-parity to auto-routing improved with the more robust route definitions. This adds provides the following 3 attributes that can be used by any controller (not just auto-routing):

  • Filter: applies any of the defined filters with optional parameters
  • Restrict: restrict the controller/method by environment, hostname, or subdomain
  • Cache: this is a new feature, but will automatically handle caching the results of the method server-side.

This also brings some of these behaviors closer to the location where they should be run, making it more obvious what's being processed where.

Examples:

#[Filter(by: 'group', having: ['admin', 'superadmin'])]
class AdminController extends BaseController
{
    #[Filter(by: 'permission', having: ['users.manage'])]
    public function users()
    {
        // Will have 'group' filter with ['admin', 'superadmin']
        // and 'permission' filter with ['users.manage']
    }
}
#[Restrict(environment: ['development', '!production'])]
class HomeController extends BaseController
{
    // Restrict access by hostname
    #[Restrict(hostname: 'localhost')]
    public function index()
    {
    }

    // Multiple allowed hosts
    #[Restrict(hostname: ['localhost', '127.0.0.1', 'dev.example.com'])]
    public function devOnly()
    {
    }

    // Restrict to subdomain, e.g. admin.example.com
    #[Restrict(subdomain: 'admin')]
    public function deleteItem($id)
    {
    }
}
class HomeController extends BaseController
{
    // Cache this method's response for 2 hours
    #[Cache(for: 2 * HOUR)]
    public function index()
    {
        return view('welcome_message');
    }

    // Custom cache key
    #[Cache(for: 10 * MINUTE, key: 'custom_cache_key')]
    public function custom()
    {
        return 'This response is cached with a custom key for 10 minutes.';
    }
}

Checklist:

  • Securely signed commits
  • Component(s) with PHPDoc blocks, only if necessary or adds value (without duplication)
  • Unit testing, with >80% coverage
  • User guide updated
  • Conforms to style guide

@lonnieezell lonnieezell requested a review from michalsn October 5, 2025 22:47
Copy link
Member

@michalsn michalsn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this looks like a really nice feature to have in the framework.

#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class Restrict implements RouteAttributeInterface
{
private const TWO_PART_TLDS = [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should extract this list into a separate config file. There are a lot more domains like this (e.g., com.pl, gov.pl, and regional ones like poznan.pl), and that's just from my country.

There's no point in trying to hardcode every possible combination here. Moving it into a config file would allow developers to add or override entries as needed.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good call. i might take another look at better detecting subdomains. I just realized I forgot to look how the router was currently doing it like I was planning on. This doesn't feel wonderful but it does seem the most accurate way I could come up with at the time.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I'm looking in the right place, then it seems like the Router is doing this even simpler: https://github.com/codeigniter4/CodeIgniter4/blob/develop/system/Router/RouteCollection.php#L1665

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is doing it simpler. And only account for co.* doubles. Not great. I think what needs to happen is that after this is approved and merged we need to create a new method in the url_helper to detect subdomains, and use a config file for more robust double detections.

Comment on lines +791 to +827
$reflectionClass = new ReflectionClass($this->controller);

// Process class-level attributes
foreach ($reflectionClass->getAttributes() as $attribute) {
try {
$instance = $attribute->newInstance();

if ($instance instanceof RouteAttributeInterface) {
$this->routeAttributes['class'][] = $instance;
}
} catch (Throwable) {
log_message('error', 'Failed to instantiate attribute: ' . $attribute->getName());
}
}

if ($this->method === '' || $this->method === null) {
return;
}

// Process method-level attributes
if ($reflectionClass->hasMethod($this->method)) {
$reflectionMethod = $reflectionClass->getMethod($this->method);

foreach ($reflectionMethod->getAttributes() as $attribute) {
try {
$instance = $attribute->newInstance();

if ($instance instanceof RouteAttributeInterface) {
$this->routeAttributes['method'][] = $instance;
}
} catch (Throwable) {
// Skip attributes that fail to instantiate
log_message('error', 'Failed to instantiate attribute: ' . $attribute->getName());
}
}
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From a performance perspective, if the developer knows they will not use attributes, it might be worth adding an option to skip calling this method entirely. Currently, we perform reflection on both the class and the method for every HTTP request, which could be expensive.

Another option would be to cache these results, at least in the production environment.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A config option could work for now. I tried to limit the amount of reflection happening to just those requests, instead of something like scanning all routes files, etc. But a config option is cheap and easy. I definitely think there's more we can do toward optimizing performance. I've been thinking on that for a little bit. More to come in the future?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The simpler options seem right for a starter.

@michalsn michalsn added new feature PRs for new features 4.7 GPG-Signing needed Pull requests that need GPG-Signing labels Oct 6, 2025
@neznaika0
Copy link
Contributor

It would be worth combining together with https://github.com/kenjis/ci4-attribute-routes
You can improve a lot of things this way.

However, the use cases are unclear to me. I have never used cache or restrictions.

@lonnieezell
Copy link
Member Author

It would be worth combining together with https://github.com/kenjis/ci4-attribute-routes You can improve a lot of things this way.

I hadn't seen that one. That's an interesting use case! Since that's not a run-time use, though, it handles a different use case, so probably doesn't make sense to merge features. But thanks for sharing it! I need to play around with that at some point.

However, the use cases are unclear to me. I have never used cache or restrictions.

The cache attribute is an idea I've had for a while now to make it really simple to cache whole endpoints without all of the boilerplate. It never made sense on it's own, though. I recently starting thinking more about where CI fits in the marketplace, where it can be simpler, etc. and I think I may have led us a little astray when I pushed the route declaration files in 4.0 as the primary way to route. Kenjis auto-routing improved that they added is a great middle-ground for ease of use and security.

This was an attempt to get auto-routing to feature parity with route declaration files. Many of the other features are already handled by file-based auto-routing (like prefixes, namespaces, etc). Once I get to some doc rewrites that are coming up, I want to present auto-routing first, since it is easy to understand that mapping of files, I think. As a bonus - auto-routing should have higher performance, though I need to look at one thing with it that I know if.

As for usefulness of the features? I think they're pretty handy. I've occassionally built dev-only pages/tools and use the environment restrictions to do that. Caching can be a huge win. And if we want auto-routing to be the primary way, it really needed a nicer interface with the controller filters.

@neznaika0
Copy link
Contributor

I remember being told that it's better to have a backup value. In case of compatibility.
See in code:
$oldFilterOrder = config(Feature::class)->oldFilterOrder ?? false;
But otherwise, you don't have to compare true/false, it's better to ! config(Routing::class)->useControllerAttributes

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
4.7 GPG-Signing needed Pull requests that need GPG-Signing new feature PRs for new features
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants