{"id":21591956,"url":"https://github.com/remotelyliving/doorkeeper","last_synced_at":"2025-04-10T22:53:23.024Z","repository":{"id":20829888,"uuid":"90170961","full_name":"remotelyliving/doorkeeper","owner":"remotelyliving","description":"A Feature Toggle for PHP","archived":false,"fork":false,"pushed_at":"2023-04-19T19:36:11.000Z","size":168,"stargazers_count":16,"open_issues_count":4,"forks_count":2,"subscribers_count":6,"default_branch":"master","last_synced_at":"2025-04-10T22:53:15.317Z","etag":null,"topics":["feature-flags","feature-toggle","feature-toggles"],"latest_commit_sha":null,"homepage":"","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/remotelyliving.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2017-05-03T16:36:31.000Z","updated_at":"2022-10-30T16:45:04.000Z","dependencies_parsed_at":"2024-11-24T16:45:53.846Z","dependency_job_id":null,"html_url":"https://github.com/remotelyliving/doorkeeper","commit_stats":{"total_commits":36,"total_committers":1,"mean_commits":36.0,"dds":0.0,"last_synced_commit":"9f84115d5b8cdb04e2d7ce5bc0c7349cd666dca1"},"previous_names":[],"tags_count":8,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/remotelyliving%2Fdoorkeeper","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/remotelyliving%2Fdoorkeeper/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/remotelyliving%2Fdoorkeeper/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/remotelyliving%2Fdoorkeeper/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/remotelyliving","download_url":"https://codeload.github.com/remotelyliving/doorkeeper/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248312172,"owners_count":21082638,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["feature-flags","feature-toggle","feature-toggles"],"created_at":"2024-11-24T16:34:59.226Z","updated_at":"2025-04-10T22:53:22.999Z","avatar_url":"https://github.com/remotelyliving.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![Build Status](https://travis-ci.org/remotelyliving/doorkeeper.svg?branch=master)](https://travis-ci.org/remotelyliving/doorkeeper)\n[![Total Downloads](https://poser.pugx.org/remotelyliving/doorkeeper/downloads)](https://packagist.org/packages/remotelyliving/doorkeeper)\n[![Coverage Status](https://coveralls.io/repos/github/remotelyliving/doorkeeper/badge.svg?branch=master)](https://coveralls.io/github/remotelyliving/doorkeeper?branch=master) \n[![License](https://poser.pugx.org/remotelyliving/doorkeeper/license)](https://packagist.org/packages/remotelyliving/doorkeeper)\n[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/remotelyliving/doorkeeper/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/remotelyliving/doorkeeper/?branch=master)\n\n# Doorkeeper: a dynamic feature toggle\n\n### The Birth of a Feature Toggle\n\u003ePicture the scene. You're on one of several teams working on a sophisticated town planning simulation game. Your team is responsible for the core simulation engine. You have been tasked with increasing the efficiency of the Spline Reticulation algorithm. You know this will require a fairly large overhaul of the implementation which will take several weeks. Meanwhile other members of your team will need to continue some ongoing work on related areas of the codebase.\nYou want to avoid branching for this work if at all possible, based on previous painful experiences of merging long-lived branches in the past. Instead, you decide that the entire team will continue to work on trunk, but the developers working on the Spline Reticulation improvements will use a Feature Toggle to prevent their work from impacting the rest of the team or destabilizing the codebase.\n\nhttps://martinfowler.com/articles/feature-toggles.html\n\n### Enter Doorkeeper (if you can)\n\nThere are a few feature toggle frameworks and libraries out there already. And many of them are fine.\nDoorkeeper was born our of a previous experience with one and wishing what it could be.\n\n### Dynamic Usage\n\nDoorkeeper is storage agnostic, and has a few helpers to help translate what you decide to persist\nand how you want to load it. But however you choose to setup its Feature Set (features + rules), you can toggle your feature on and off\nby changing that configuration.\n\n### Installation\n\n`composer require remotelyliving/doorkeeper`\n\n### Wiring it up\n\n```php\n// Requestors are the actor requesting access to a new feature\n// They have several forms of identity you can initialize them\n$requestor = ( new Requestor() )\n    -\u003ewithUserId($user-\u003egetId())\n    -\u003ewithRequest($request)\n    -\u003ewithIpAddress('127.0.0.1')\n    -\u003ewithEnvironment('STAGE')\n    -\u003ewithStringHash('someArbitraryThingMaybeFromTheQueryString');\n \n// A Feature Set has Features that have Rules     \n$featureSet = $reatureSetRepository-\u003egetSet();\n\n// Doorkeper takes in a Feature Set and an audit log if you want to log access results\n$doorkeeper = new Doorkeeper($featureSet, $logger);\n\n// Set an app instance bound requestor here or pass one to Doorkeeper::grantsAccessToRequestor('feature', $requestor) later\n$doorkeeper-\u003esetRequestor($requestor);\n```\n\n### Usage\n\n```php\nif ($doorkeeper-\u003egrantsAccessTo('some.new.feature')) {\n    return $this-\u003edoNewFeatureStuff();\n}\n\n// If you want to bypass the instance Requestor that was set and create another use Doorkeeper::grantsAccessToRequestor()\n// This is useful for more stateful applications\n\n$otherRequestor = (new Requestor()))-\u003ewithUserId(123);\n\nif ($doorkeeper-\u003egrantsAccessToRequestor('some.new.feature', $otherRequestor)) {\n    return $this-\u003edoNewFeatureStuff();\n}\n```\n\n***Setting a Requestor is not neccessary. It is only needed if you want to use rules that are evaluated\nagainst a specific requestor***\n\n### Requestor\n\nA Requestor is the one asking to access the feature. They must pass the Doorkeeper's strict house rules to enter.\nTo see if a requestor is allowed access, they must present Identifications.\n\n`RemotelyLiving\\Doorkeeper\\Identifications`\n\n- HttpHeader - based on a `doorkeeper` header present in a request.\n- IntegerId (user id) - Based on the logged in user id\n- IpAddress - based on the Requestor's ip address\n- StringHash - Some flexibility here. Set it to whatever you want.\n- Environment - Based on the app environment the Requestor is in.\n- PipedComposite = A string of pipe delimited values used to make a composite key id\n\nThe Requestor is immutable. It should not be changed anywhere in the call stack. \nThat would produce less than consistent results depending on where the query takes place.\n\nThe Requestor is best wired up and set in a service container. There are several convenience methods\nto set identities.\n\n### Rules\n\n`RemotelyLiving\\Doorkeeper\\Rules`\n\nThere are several types of Rules to use when defining access to a feature\n\n- Environment: can be set with the environment name that the feature is available in\n\n- HttpHeader: matches a specific value from a custom `doorkeeper` header you can choose to send.\n *The request identification on a Requestor must be registered to have a positive match\n\n- IpAddress: this is a specific IP address the feature is available to. Helpful for in-office access.\n *It only works if the Requestor has an IpAddress identification registered\n  \n- Percentage: a percentage of requests to allow through to the feature\n\n- Random: chaos monkey. This rule randomly allows access.\n\n- StringHash: this is an arbitrary string. It works well for request query params, username, etc.\n *This only works if a StringHash identification is set on the Requestor\n\n- TimeAfter: allows for access to a feature only *after* the set time on the rule.\n\n- TimeBefore: allows for access to a feature only *before* the set time of the rule.\n\n- UserId: this rule allows for specific user access to a feature.\n *User Id only works if the Request has a user id identification registered to them\n \n- PipedComposite: allows for a pipe delimited composite key value \n\n- RuntimeCallable: pass in a callable that returns true/false. You'll have access to the requestor as a parameter, but be forewarned that storing this one is not possible (i.e. to a database) \n\n### Prerequisistes\n\nRules can be dependant on other rules for any other feature.\n\n```php\n// create a time range\n$timeBeforeRule-\u003eaddPrerequisite($timeAfterRule);\n\n// create a user id rule only for prod\n$userIdRule-\u003eaddPrerequisite($prodEnvironmentRule);\n\n// add another prereq\n$userIdRule-\u003eaddPrerequisite($ipAddressRule);\n\n// etc.\n```\n\nThat prerequisite must be satisfied before the other rule is evaluated.\n\n### Feature\n\n`RemotelyLiving\\Doorkeeper\\Features`\n\nA Feature is what a Requestor is asking for by name. It can have 0-n Rules around it.\nA Feature has a top level on/off switch called `enabled` that can bypass any rules.\n\n```php\n// $enabled (true/false), $rules (\\RemotelyLiving\\Doorkeeper\\Rules\\RuleInterface[])\n$feature = new Feature('some.new.feature', $enabled, $rules);\n```\n\nDoorkeeper gets the rules from a feature and evaluates them. If they require a specific Identification\nDoorkeeper looks into the Requestor to see if they have the right Identifications required by a rule\n\nIf nothing satisfies the feature rules the default is to deny access.\n\n***The first rule to be satisfied grants access***\n\nKeep that in mind when setting rules up on a feature.\n\nIf you say \"only this ip address is allowed, but also the DEV environment.\"\n\nAll requests with that ip address OR in the DEV environment will be allowed.\n\nBut that means that in ANY environment, that ip address will be allowed.\n\nThe proper way to setup exclusions would be to set the ip address rule as a prerequisite rule to the environment one.\nThat would then stipulate that only this ip address in this environment can access.\n\nA Feature with no rules simply relies on the `enabled` field to tell Doorkeeper if it's on or off\n\n### Feature Set\n\nA feature set object is the bread and butter of Doorkeeper. It's a collection of Features and rules and makes for easy\ncaching. It is the complete set of what defines access to features.\n\n### Feature Set Repository and Caching\n\n`Remotelyliving\\Doorkeeper\\Features`\n\nObviously something that fires up for every request that uses dynamic config data can be costly.\n\nIf you're using memcached or redis and a PSR6 cache library. you can cache and retrieve the Feature Set in the `Features\\SetRepository`.\n \nYou can build the Feature Set from arrays using the `Features\\Set::createFromArray()`\n\nThat array can come from anywhere: relational database, cache, config, xml, whatever.\n\nCheckout that factory method to see the schema of the array that needs to be passed in.\n\nHow you choose to persist Features is up to you. But there are two things you're responsible for if using the `Features\\SetRepository`\n\n1. Clearing the cache when any member of a Feature Set is changed via `SetRepository::deleteFeatureSet()`\n2. Providing a service that can provide a hydrated Feature Set to the `get('some.new.feature', $featureSetProvider)` method\n\nDoorkeeper also has a runtime cache that caches answers in memory to help as well.\nFor persistent applications you'll need to call `Doorkeeper::flushRuntimeCache()`\nany time a Rule or Feature is updated.\n\n### Logging\n\nDoorkeeper comes with a friendly log processor that can pass on filtered or unfiltered info about a Requestor.\nThis is incredibly helpful when debugging. \nWhen paired with a Request-Id (or something like that) in the log context debugging a user's code patch can be very easy.\n\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fremotelyliving%2Fdoorkeeper","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fremotelyliving%2Fdoorkeeper","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fremotelyliving%2Fdoorkeeper/lists"}