{"id":45066891,"url":"https://github.com/veronalabs/wp-scoper","last_synced_at":"2026-05-04T08:03:51.707Z","repository":{"id":339191601,"uuid":"1160852635","full_name":"veronalabs/wp-scoper","owner":"veronalabs","description":"A Composer plugin that prefixes namespaces in WordPress plugin dependencies to prevent conflicts","archived":false,"fork":false,"pushed_at":"2026-05-02T05:56:29.000Z","size":109,"stargazers_count":5,"open_issues_count":0,"forks_count":1,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-02T07:11:26.655Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/veronalabs.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"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,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-02-18T12:57:20.000Z","updated_at":"2026-05-02T05:56:18.000Z","dependencies_parsed_at":"2026-02-22T14:01:31.529Z","dependency_job_id":null,"html_url":"https://github.com/veronalabs/wp-scoper","commit_stats":null,"previous_names":["veronalabs/wp-scoper"],"tags_count":14,"template":false,"template_full_name":null,"purl":"pkg:github/veronalabs/wp-scoper","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/veronalabs%2Fwp-scoper","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/veronalabs%2Fwp-scoper/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/veronalabs%2Fwp-scoper/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/veronalabs%2Fwp-scoper/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/veronalabs","download_url":"https://codeload.github.com/veronalabs/wp-scoper/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/veronalabs%2Fwp-scoper/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32599416,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-03T22:12:39.696Z","status":"online","status_checked_at":"2026-05-04T02:00:06.625Z","response_time":58,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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":[],"created_at":"2026-02-19T11:04:41.997Z","updated_at":"2026-05-04T08:03:51.680Z","avatar_url":"https://github.com/veronalabs.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# WP Scoper\n\nA Composer plugin that prefixes namespaces in WordPress plugin dependencies to prevent fatal PHP conflicts.\n\n## The Problem\n\nWordPress plugins sharing the same Composer dependencies (e.g., Guzzle, GeoIP2) cause fatal errors because PHP cannot load two versions of the same class. When Plugin A requires `geoip2/geoip2 ^2.0` and Plugin B requires `geoip2/geoip2 ^3.0`, one will crash.\n\nWP Scoper solves this by adding a unique prefix to all namespaces in your vendored dependencies:\n\n```\nGeoIp2\\Database\\Reader → WP_Statistics\\Deps\\GeoIp2\\Database\\Reader\n```\n\nEach plugin gets its own isolated copy of dependencies. No conflicts.\n\n## Comparison\n\n| Feature | WP Scoper | Mozart | PHP-Scoper |\n|---|---|---|---|\n| Installation | `composer require --dev` | Global / PHAR | Global / PHAR |\n| Works on any machine | ✅ Yes | ❌ No | ❌ No |\n| Composer plugin | ✅ Yes (auto-runs) | ❌ No | ❌ No |\n| Namespace prefixing | ✅ Yes | ✅ Yes | ✅ Yes |\n| Global class prefixing | ✅ Yes | ✅ Yes | ✅ Yes |\n| Constant prefixing | ✅ Yes | ❌ No | ✅ Yes |\n| Template/view safety | ✅ Auto-detect + config | ❌ No | ⚠️ Manual exclude |\n| WordPress function safety | ✅ Safe | ✅ Safe | ❌ Breaks WP functions |\n| Property name bug | ✅ Fixed | ❌ Had bugs | ➖ N/A (AST) |\n| Autoloader generation | ✅ Yes (classmap) | ❌ No | ⚠️ Partial |\n| Dev-dependency support | ✅ Yes | ❌ No | ❌ No |\n| Update host source files | ✅ Yes | ❌ No | ➖ N/A |\n| PHP version support | 7.4+ | 8.1+ | 7.2+ |\n\n## Installation\n\n```bash\ncomposer require --dev veronalabs/wp-scoper\n```\n\nAdd configuration to your `composer.json`:\n\n```json\n{\n    \"require\": {\n        \"geoip2/geoip2\": \"^2.0\"\n    },\n    \"require-dev\": {\n        \"veronalabs/wp-scoper\": \"^1.0\"\n    },\n    \"extra\": {\n        \"wp-scoper\": {\n            \"namespace_prefix\": \"WP_Statistics\\\\Deps\",\n            \"packages\": [\"geoip2/geoip2\"]\n        }\n    }\n}\n```\n\nRun `composer install` or `composer wp-scope`. That's it.\n\n## Output\n\nAfter running, WP Scoper displays a summary of what was done:\n\n```\n+------------------------------------------------+\n| WP Scoper - Your dependencies, your namespace! |\n+---------------------+--------------------------+\n| Packages            | 7                        |\n| PHP Files Prefixed  | 90                       |\n| Files Excluded      | 60                       |\n| Namespaces Prefixed | 6                        |\n| Global Classes      | 3                        |\n| Constants           | 0                        |\n| Call Sites Updated  | 0                        |\n| Output Size         | 2.3 MB / 2.7 MB (-14%)   |\n| Target Directory    | packages                 |\n+------------------------------------------------+\n```\n\n| Stat | Description |\n|---|---|\n| **Packages** | Number of vendor packages processed (including transitive dependencies) |\n| **PHP Files Prefixed** | PHP files copied and namespace-prefixed |\n| **Files Excluded** | Files skipped by built-in and custom exclude patterns (tests, docs, configs) |\n| **Namespaces Prefixed** | Unique vendor namespaces that were rewritten |\n| **Global Classes** | Non-namespaced classes that were prefixed (e.g. `Spyc` -\u003e `WP_Statistics_Spyc`) |\n| **Constants** | `define()` constants that were prefixed |\n| **Call Sites Updated** | Files in your `src/` whose `use` statements were auto-updated |\n| **Output Size** | Output size vs original size with reduction percentage |\n| **Target Directory** | Where prefixed files were written |\n\n## How It Works\n\n1. Reads your `extra.wp-scoper` config from `composer.json`\n2. Discovers listed packages and their transitive dependencies\n3. Copies them to a target directory (default: `vendor-prefixed/`)\n4. Prefixes all namespaces, global classes, and constants\n5. Generates a classmap-based autoloader\n6. Optionally updates `use` statements in your own source files\n\n## Configuration Reference\n\n| Option | Required | Default | Description |\n|---|---|---|---|\n| `namespace_prefix` | **Yes** | - | Prefix for all namespaces (e.g., `WP_Statistics\\\\Deps`) |\n| `packages` | **Yes** | - | Which vendor packages to prefix (transitive deps auto-included) |\n| `target_directory` | No | `vendor-prefixed` | Where prefixed files are copied |\n| `class_prefix` | No | Auto-derived | Prefix for global (non-namespaced) classes |\n| `constant_prefix` | No | Auto-derived | Prefix for `define()` constants |\n| `exclude_packages` | No | `[]` | Transitive deps to skip |\n| `exclude_patterns` | No | `[]` | Additional regex patterns for files to skip (merged with built-in patterns) |\n| `exclude_directories` | No | `[\"views\", \"templates\", \"resources\"]` | Directories with template files (copied but not prefixed) |\n| `delete_vendor_packages` | No | `false` | Remove originals from `vendor/` after copy |\n| `update_call_sites` | No | `true` | Update `use` statements in host project files. `true` scans `src/`, or pass an array of directories (see below) |\n| `dev_packages` | No | `null` | Config for prefixing `require-dev` packages separately |\n\n### Full Configuration Example\n\n```json\n{\n    \"extra\": {\n        \"wp-scoper\": {\n            \"target_directory\": \"packages\",\n            \"namespace_prefix\": \"WP_Statistics\\\\Deps\",\n            \"class_prefix\": \"WP_Statistics_\",\n            \"constant_prefix\": \"WP_STATISTICS_\",\n            \"packages\": [\"geoip2/geoip2\", \"matomo/device-detector\"],\n            \"exclude_packages\": [\"psr/log\"],\n            \"exclude_patterns\": [\"/\\\\.md$/\", \"/tests?\\\\//i\"],\n            \"exclude_directories\": [\"views\", \"templates\", \"resources\"],\n            \"delete_vendor_packages\": false,\n            \"update_call_sites\": true,\n            \"dev_packages\": {\n                \"enabled\": true,\n                \"target_directory\": \"tests/vendor-prefixed\",\n                \"packages\": [\"fakerphp/faker\"]\n            }\n        }\n    }\n}\n```\n\n## Project Layout\n\nRecommended layout with `vendor/` gitignored:\n\n```\nmy-plugin/\n├── .gitignore              # /vendor/ is gitignored\n├── composer.json\n├── vendor/                 # NOT committed\n│   └── autoload.php\n├── packages/               # COMMITTED - prefixed vendor packages\n│   ├── autoload.php        # Generated by wp-scoper\n│   ├── geoip2/\n│   └── maxmind/\n├── src/\n│   └── Plugin.php          # Your code\n└── tests/\n```\n\nIn your plugin's main file:\n\n```php\nrequire_once __DIR__ . '/packages/autoload.php';\n\nuse WP_Statistics\\Deps\\GeoIp2\\Database\\Reader;\n\n$reader = new Reader('/path/to/GeoLite2-City.mmdb');\n```\n\n## Usage\n\n### As a Composer Plugin (automatic)\n\nWP Scoper runs automatically after `composer install` and `composer update`.\n\n### Manual Command\n\n```bash\ncomposer wp-scope\ncomposer wp-scope --dry-run\n```\n\n### Standalone CLI\n\n```bash\nvendor/bin/wp-scoper\nvendor/bin/wp-scoper /path/to/project\nvendor/bin/wp-scoper --dry-run\n```\n\n## Automatic Call Site Updates (`update_call_sites`)\n\nWhen dependencies get prefixed, your own source code still references the original namespaces. Normally you'd have to manually find and replace every `use` statement, every `new` call, and every type hint across your entire project. With `update_call_sites` enabled (the default), wp-scoper does this for you automatically.\n\nBy default it scans all PHP files in your `src/` directory. You can also pass an array of directories to scan additional locations:\n\n```json\n{\n    \"update_call_sites\": [\"src\", \"includes\"]\n}\n```\n\nYou write your code using the original package namespaces, and wp-scoper handles the rest.\n\n### Example: Before and After\n\nSay you have this file in your project:\n\n**`src/GeoIP/GeoIPService.php` - before running wp-scoper:**\n\n```php\n\u003c?php\n\nnamespace WP_Statistics\\GeoIP;\n\nuse GeoIp2\\Database\\Reader;\nuse GeoIp2\\Model\\City;\nuse GeoIp2\\Exception\\AddressNotFoundException;\n\nclass GeoIPService\n{\n    /** @var Reader */\n    private $reader;\n\n    public function __construct(string $dbPath)\n    {\n        $this-\u003ereader = new Reader($dbPath);\n    }\n\n    public function lookup(string $ip): ?City\n    {\n        try {\n            return $this-\u003ereader-\u003ecity($ip);\n        } catch (AddressNotFoundException $e) {\n            return null;\n        }\n    }\n}\n```\n\n**After running `composer install` (or `composer wp-scope`), wp-scoper automatically changes it to:**\n\n```php\n\u003c?php\n\nnamespace WP_Statistics\\GeoIP;\n\nuse WP_Statistics\\Deps\\GeoIp2\\Database\\Reader;\nuse WP_Statistics\\Deps\\GeoIp2\\Model\\City;\nuse WP_Statistics\\Deps\\GeoIp2\\Exception\\AddressNotFoundException;\n\nclass GeoIPService\n{\n    /** @var Reader */\n    private $reader;\n\n    public function __construct(string $dbPath)\n    {\n        $this-\u003ereader = new Reader($dbPath);\n    }\n\n    public function lookup(string $ip): ?City\n    {\n        try {\n            return $this-\u003ereader-\u003ecity($ip);\n        } catch (AddressNotFoundException $e) {\n            return null;\n        }\n    }\n}\n```\n\nNotice:\n- All three `use` statements were updated automatically\n- Your own namespace (`WP_Statistics\\GeoIP`) was **not** touched\n- `$this-\u003ereader` was **not** touched\n- Local aliases (`Reader`, `City`, `AddressNotFoundException`) still work as before\n- You never had to manually search/replace anything\n\n### What gets updated in your source files\n\n- `use GeoIp2\\...` → `use WP_Statistics\\Deps\\GeoIp2\\...`\n- `new \\GeoIp2\\Database\\Reader()` → `new \\WP_Statistics\\Deps\\GeoIp2\\Database\\Reader()`\n- `@param GeoIp2\\Model\\City` → `@param WP_Statistics\\Deps\\GeoIp2\\Model\\City`\n- Fully qualified class references, type hints, catch blocks, etc.\n\n### When to disable it\n\nSet `\"update_call_sites\": false` if:\n- You prefer to manage namespace references manually\n- Your source files are outside `src/` and you handle updates yourself\n- You're running wp-scoper in a CI pipeline where source files shouldn't be modified\n\n## Template Safety\n\nWP Scoper auto-detects template/view files (HTML mixed with PHP) and skips them during prefixing. Detection uses:\n\n1. **Directory names** - Files in `views/`, `templates/`, `resources/` (configurable)\n2. **Content analysis** - Files not starting with `\u003c?php` that contain HTML tags\n3. **HTML-to-PHP ratio** - Files with significantly more HTML than PHP\n\nTemplate files are still copied to the target directory; they just aren't modified.\n\n## What Gets Prefixed\n\n- **Namespace declarations**: `namespace GeoIp2;` → `namespace WP_Statistics\\Deps\\GeoIp2;`\n- **Use statements**: `use GeoIp2\\Database\\Reader;` → `use WP_Statistics\\Deps\\GeoIp2\\Database\\Reader;`\n- **Fully qualified names**: `new \\GeoIp2\\Database\\Reader()` → `new \\WP_Statistics\\Deps\\GeoIp2\\Database\\Reader()`\n- **Type hints and return types**\n- **String class references**: `'GeoIp2\\Database\\Reader'`\n- **PHPDoc annotations**: `@param GeoIp2\\Model\\City`\n- **Global classes**: `class MyHelper` → `class WPS_MyHelper`\n- **Constants**: `define('MY_CONST', ...)` → `define('WPS_MY_CONST', ...)`\n\n## What Does NOT Get Prefixed\n\n- Property access: `$this-\u003ereader` is never touched\n- Variable names: `$reader` is never touched\n- Array keys: `$data['reader']` is never touched\n- WordPress functions: `add_action`, `get_option`, etc. are safe\n- PHP built-in classes: `stdClass`, `DateTime`, `Exception`, etc.\n- PHP built-in constants: `PHP_EOL`, `DIRECTORY_SEPARATOR`, etc.\n- Files matching `exclude_patterns`\n- Template/view files (auto-detected)\n\n## Dev-Dependencies Support\n\nPrefix `require-dev` packages into a separate directory for test isolation:\n\n```json\n{\n    \"dev_packages\": {\n        \"enabled\": true,\n        \"target_directory\": \"tests/vendor-prefixed\",\n        \"packages\": [\"fakerphp/faker\"]\n    }\n}\n```\n\nThis generates a separate autoloader at `tests/vendor-prefixed/autoload.php` loaded only during testing.\n\n## Standalone Autoloader (No `vendor/` in Production)\n\nWordPress plugins submitted to wordpress.org can't ship a `vendor/` directory. WP Scoper solves this by generating a standalone autoloader that handles **both** your own classes and prefixed dependencies — no `vendor/autoload.php` needed.\n\nWP Scoper automatically reads your project's `autoload.psr-4` from `composer.json` and includes it in the generated autoloader. For example, with this config:\n\n```json\n{\n    \"autoload\": {\n        \"psr-4\": {\n            \"WP_SMS\\\\\": \"src/\"\n        }\n    },\n    \"extra\": {\n        \"wp-scoper\": {\n            \"namespace_prefix\": \"WP_SMS\\\\Deps\",\n            \"packages\": [\"geoip2/geoip2\"],\n            \"target_directory\": \"packages\"\n        }\n    }\n}\n```\n\nThe generated `packages/autoload.php` will autoload:\n1. **Prefixed dependencies** via classmap (`WP_SMS\\Deps\\GeoIp2\\...`)\n2. **Your own classes** via PSR-4 (`WP_SMS\\...` → `src/`)\n\nIn your plugin, you only need one require:\n\n```php\nrequire_once __DIR__ . '/packages/autoload.php';\n```\n\nThis single file replaces `vendor/autoload.php` entirely. Ship `packages/` in your plugin zip, keep `vendor/` in `.gitignore`.\n\n## Requirements\n\n- PHP 7.4+\n- Composer 2.0+\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fveronalabs%2Fwp-scoper","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fveronalabs%2Fwp-scoper","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fveronalabs%2Fwp-scoper/lists"}