diff --git a/DEPRECATED.md b/DEPRECATED.md index 5c1af24f..d250b71f 100644 --- a/DEPRECATED.md +++ b/DEPRECATED.md @@ -2,8 +2,55 @@ This file lists documentation that has been consolidated, superseded, or is no longer maintained. -**Last Updated**: 2026-01-27 -**Status**: Phases 1, 2, and 3 completed. Code cleanup for deprecated methods completed. +**Last Updated**: 2026-01-31 +**Status**: Phases 1, 2, and 3 completed. Code cleanup for deprecated methods completed. New Deprecated attribute available. + +--- + +## 🆕 New in v3.3: Deprecated Attribute + +### PHP Attribute for Marking Deprecated Code + +A new `#[Deprecated]` attribute is now available for marking deprecated code elements with structured metadata: + +**Location**: `App\Attributes\Deprecated` + +**Features**: +- **GUID Tracking**: Unique identifier for each deprecated element (e.g., `DEP-2024-001`) +- **Documentation**: Clear message explaining why something is deprecated +- **Version Info**: Track when deprecation occurred (`since`) +- **Migration Path**: Suggest alternatives (`alternative`) +- **Removal Planning**: Indicate when removal is planned (`removeIn`) + +**Usage Example**: +```php +use App\Attributes\Deprecated; + +#[Deprecated( + guid: 'DEP-2024-001', + message: 'Use the new implementation instead', + since: 'v1.0.0', + alternative: 'NewClass::newMethod()', + removeIn: 'v2.0.0' +)] +public function oldMethod(): void +{ + // deprecated implementation +} +``` + +**Supported Targets**: +- Classes +- Methods +- Properties +- Class Constants + +**Benefits over `@deprecated` DocBlock**: +- Structured data that can be programmatically analyzed +- GUID-based tracking across codebase +- Type-safe with PHP 8 attributes +- Consistent deprecation metadata format +- Can be queried via reflection API --- @@ -166,6 +213,43 @@ Update code comments referencing old documentation: // See ROLE_SYSTEM.md for role hierarchy ``` +### Migrating from @deprecated to #[Deprecated] Attribute + +**Old Style** (DocBlock annotation): +```php +/** + * @deprecated since version 1.0 (2026-01-30). Use is_subscriber flag instead. + * Will be removed in version 2.0. + */ +public const OPERATOR_LEVEL_CUSTOMER = 100; +``` + +**New Style** (PHP 8 Attribute): +```php +use App\Attributes\Deprecated; + +#[Deprecated( + guid: 'DEP-2026-001', + message: 'Use is_subscriber flag instead', + since: 'v1.0.0 (2026-01-30)', + removeIn: 'v2.0.0' +)] +public const OPERATOR_LEVEL_CUSTOMER = 100; +``` + +**Benefits of Migration**: +- Searchable by GUID for tracking +- Programmatic access via Reflection API +- Type-safe and validated at parse time +- Consistent structure across codebase +- Can generate deprecation reports + +**Migration Strategy**: +1. New deprecations should use `#[Deprecated]` attribute +2. Existing `@deprecated` can be migrated gradually +3. Both styles are acceptable during transition period +4. Update `@deprecated` to attribute when touching that code + --- ## Removed Files diff --git a/app/Attributes/Deprecated.php b/app/Attributes/Deprecated.php new file mode 100644 index 00000000..9016026d --- /dev/null +++ b/app/Attributes/Deprecated.php @@ -0,0 +1,97 @@ +guid}]"; + + if ($this->since) { + $parts[] = "since {$this->since}"; + } + + $parts[] = "- {$this->message}"; + + if ($this->alternative) { + $parts[] = "Use {$this->alternative} instead."; + } + + if ($this->removeIn) { + $parts[] = "Will be removed in {$this->removeIn}."; + } + + return implode(' ', $parts); + } + + /** + * Convert to array representation. + * + * @return array + */ + public function toArray(): array + { + return [ + 'guid' => $this->guid, + 'message' => $this->message, + 'since' => $this->since, + 'alternative' => $this->alternative, + 'removeIn' => $this->removeIn, + 'formattedMessage' => $this->getFormattedMessage(), + ]; + } +} diff --git a/app/Attributes/README.md b/app/Attributes/README.md new file mode 100644 index 00000000..cc84153f --- /dev/null +++ b/app/Attributes/README.md @@ -0,0 +1,383 @@ +# Deprecated Attribute + +A PHP 8 attribute for marking deprecated code elements with structured metadata and GUID tracking. + +## Overview + +The `Deprecated` attribute provides a modern, type-safe way to mark code as deprecated with comprehensive metadata. It offers significant advantages over traditional `@deprecated` DocBlock comments by providing structured, programmatically accessible deprecation information. + +## Location + +``` +App\Attributes\Deprecated +``` + +## Features + +- **GUID Tracking**: Unique identifier for each deprecated element +- **Structured Metadata**: Version info, alternatives, and removal plans +- **Type-Safe**: PHP 8 attribute with compile-time validation +- **Reflection API**: Programmatically query deprecation information +- **Multiple Targets**: Works with classes, methods, properties, and constants + +## Usage + +### Basic Example + +```php +use App\Attributes\Deprecated; + +#[Deprecated( + guid: 'DEP-2024-001', + message: 'This method is no longer maintained' +)] +public function oldMethod(): void +{ + // deprecated implementation +} +``` + +### Complete Example with All Parameters + +```php +#[Deprecated( + guid: 'DEP-2024-002', + message: 'Use the new API implementation instead', + since: 'v1.0.0', + alternative: 'NewClass::newMethod()', + removeIn: 'v2.0.0' +)] +public function legacyMethod(): void +{ + // deprecated implementation +} +``` + +### Supported Targets + +#### 1. Methods + +```php +#[Deprecated( + guid: 'DEP-2024-003', + message: 'Replaced by faster implementation', + alternative: 'optimizedMethod()' +)] +public function slowMethod(): void +{ + // old implementation +} +``` + +#### 2. Classes + +```php +#[Deprecated( + guid: 'DEP-2024-004', + message: 'Use the redesigned service class', + since: 'v2.0.0', + alternative: 'App\Services\NewService' +)] +class LegacyService +{ + // deprecated class +} +``` + +#### 3. Properties + +```php +class User +{ + #[Deprecated( + guid: 'DEP-2024-005', + message: 'Use the new address structure', + alternative: 'addressData property' + )] + public string $legacyAddress; +} +``` + +#### 4. Class Constants + +```php +class User +{ + #[Deprecated( + guid: 'DEP-2024-006', + message: 'Use is_subscriber flag instead', + since: 'v1.0.0', + removeIn: 'v2.0.0' + )] + public const OPERATOR_LEVEL_CUSTOMER = 100; +} +``` + +## Parameters + +### Required Parameters + +- **`guid`** (string): Unique identifier for tracking this deprecation + - Format: `DEP-YYYY-NNN` (e.g., `DEP-2024-001`) + - Must be unique across the entire codebase + +- **`message`** (string): Clear explanation of why this is deprecated + - Should explain the reason for deprecation + - Keep concise but informative + +### Optional Parameters + +- **`since`** (string|null): Version or date when deprecated + - Examples: `'v1.0.0'`, `'v2.5.0 (2024-01-30)'`, `'2024-01-30'` + +- **`alternative`** (string|null): Recommended replacement + - Full class/method name: `'App\Services\NewService::newMethod()'` + - Short reference: `'newMethod()'` + - Multiple alternatives: `'methodA() or methodB()'` + +- **`removeIn`** (string|null): Planned removal version + - Examples: `'v2.0.0'`, `'v3.0.0'` + - Helps users plan migration timeline + +## Methods + +### `getFormattedMessage(): string` + +Returns a human-readable deprecation message with all information: + +```php +$deprecated = new Deprecated( + guid: 'DEP-2024-001', + message: 'Old implementation', + since: 'v1.0.0', + alternative: 'newMethod()', + removeIn: 'v2.0.0' +); + +echo $deprecated->getFormattedMessage(); +// Output: "DEPRECATED [DEP-2024-001] since v1.0.0 - Old implementation Use newMethod() instead. Will be removed in v2.0.0." +``` + +### `toArray(): array` + +Converts the attribute to an associative array: + +```php +$array = $deprecated->toArray(); +/* +[ + 'guid' => 'DEP-2024-001', + 'message' => 'Old implementation', + 'since' => 'v1.0.0', + 'alternative' => 'newMethod()', + 'removeIn' => 'v2.0.0', + 'formattedMessage' => '...' +] +*/ +``` + +## Programmatic Access + +Use PHP's Reflection API to query deprecation information: + +### Get Deprecated Methods + +```php +use ReflectionClass; +use App\Attributes\Deprecated; + +$reflection = new ReflectionClass(MyClass::class); + +foreach ($reflection->getMethods() as $method) { + $attributes = $method->getAttributes(Deprecated::class); + + if (!empty($attributes)) { + $deprecated = $attributes[0]->newInstance(); + echo "Method {$method->getName()} is deprecated:\n"; + echo $deprecated->getFormattedMessage() . "\n\n"; + } +} +``` + +### Check if Element is Deprecated + +```php +$method = new ReflectionMethod(MyClass::class, 'oldMethod'); +$isDeprecated = !empty($method->getAttributes(Deprecated::class)); + +if ($isDeprecated) { + $deprecated = $method->getAttributes(Deprecated::class)[0]->newInstance(); + trigger_error($deprecated->getFormattedMessage(), E_USER_DEPRECATED); +} +``` + +## GUID Naming Convention + +Use a consistent format for GUIDs to make tracking easier: + +**Format**: `DEP-YYYY-NNN` + +- `DEP`: Prefix indicating "deprecated" +- `YYYY`: Year when deprecated +- `NNN`: Sequential number (001, 002, etc.) + +**Examples**: +- `DEP-2024-001` - First deprecation in 2024 +- `DEP-2024-002` - Second deprecation in 2024 +- `DEP-2025-001` - First deprecation in 2025 + +**Alternative formats** (also acceptable): +- `DEP-2024-USER-001` - Include component name +- `DEP-2024-Q1-001` - Include quarter + +## Migration from @deprecated + +### Before (DocBlock) + +```php +/** + * @deprecated since version 1.0 (2026-01-30). Use is_subscriber flag instead. + * Will be removed in version 2.0. + */ +public const OPERATOR_LEVEL_CUSTOMER = 100; +``` + +### After (Attribute) + +```php +use App\Attributes\Deprecated; + +/** + * @deprecated since version 1.0 (2026-01-30). Use is_subscriber flag instead. + * Will be removed in version 2.0. + */ +#[Deprecated( + guid: 'DEP-2026-001', + message: 'Customers are no longer identified by operator_level', + since: 'v1.0.0 (2026-01-30)', + alternative: 'is_subscriber flag', + removeIn: 'v2.0.0' +)] +public const OPERATOR_LEVEL_CUSTOMER = 100; +``` + +**Note**: During transition, both can coexist. Keep the `@deprecated` DocBlock for IDE support while adding the attribute for structured tracking. + +## Benefits Over @deprecated + +### Traditional @deprecated +```php +/** + * @deprecated Use newMethod() instead + */ +public function oldMethod() {} +``` + +**Limitations**: +- Unstructured text +- No standardized format +- Not programmatically accessible +- No GUID tracking +- IDE-dependent behavior + +### Deprecated Attribute +```php +#[Deprecated( + guid: 'DEP-2024-001', + message: 'Use new implementation', + alternative: 'newMethod()' +)] +public function oldMethod() {} +``` + +**Advantages**: +- ✅ Structured, typed data +- ✅ Programmatically queryable via Reflection +- ✅ GUID-based tracking +- ✅ Consistent format enforced +- ✅ Generate deprecation reports +- ✅ Build tooling for automated migration + +## Real-World Examples + +### Example 1: Deprecated Constant in User Model + +```php +use App\Attributes\Deprecated; + +class User extends Model +{ + #[Deprecated( + guid: 'DEP-2026-001', + message: 'Customers are no longer identified by operator_level', + since: 'v1.0.0 (2026-01-30)', + alternative: 'is_subscriber flag', + removeIn: 'v2.0.0' + )] + public const OPERATOR_LEVEL_CUSTOMER = 100; +} +``` + +### Example 2: Deprecated Relationship Method + +```php +#[Deprecated( + guid: 'DEP-2026-002', + message: 'Network credentials now stored directly on User model', + since: 'v1.0.0', + alternative: 'User model fields (username, service_type, etc.)' +)] +public function networkUser(): HasOne +{ + return $this->hasOne(NetworkUser::class, 'user_id'); +} +``` + +## Testing + +The attribute includes comprehensive unit tests: + +```bash +./vendor/bin/phpunit tests/Unit/Attributes/DeprecatedTest.php +``` + +Test coverage includes: +- Creating attributes with all/minimal parameters +- Formatted message generation +- Array conversion +- Application to classes, methods, properties, and constants +- Reflection API access + +## Future Enhancements + +Possible extensions for this attribute: + +1. **Automated Deprecation Reports** + - Scan codebase for all `#[Deprecated]` attributes + - Generate markdown/HTML reports by GUID + +2. **IDE Integration** + - Custom inspections that read attribute metadata + - Automated refactoring suggestions + +3. **CI/CD Checks** + - Fail builds if deprecated code exceeds threshold + - Alert on usage of code marked for imminent removal + +4. **Migration Tools** + - Automated replacement based on `alternative` field + - Track usage of deprecated elements + +## See Also + +- [DEPRECATED.md](../../DEPRECATED.md) - Project deprecation policy +- [PHP Attributes Documentation](https://www.php.net/manual/en/language.attributes.overview.php) +- [User Model](../Models/User.php) - Real-world examples + +## Questions? + +For questions about using the Deprecated attribute: +1. Review the examples in this document +2. Check [DEPRECATED.md](../../DEPRECATED.md) for project policy +3. See the unit tests for detailed usage examples +4. Open an issue on GitHub for clarification diff --git a/app/Models/User.php b/app/Models/User.php index 063ff320..b02bb0be 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -3,6 +3,7 @@ namespace App\Models; // use Illuminate\Contracts\Auth\MustVerifyEmail; +use App\Attributes\Deprecated; use App\Traits\BelongsToTenant; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -81,6 +82,13 @@ class User extends Authenticatable * Customers are no longer identified by operator_level. * Will be removed in version 2.0. */ + #[Deprecated( + guid: 'DEP-2026-001', + message: 'Customers are no longer identified by operator_level', + since: 'v1.0.0 (2026-01-30)', + alternative: 'is_subscriber flag', + removeIn: 'v2.0.0' + )] public const OPERATOR_LEVEL_CUSTOMER = 100; /** @@ -255,6 +263,12 @@ public function radiusSessions(): HasMany * This relationship maintained for backward compatibility only. * Use User model fields directly (username, service_type, etc.) */ + #[Deprecated( + guid: 'DEP-2026-002', + message: 'Network credentials now stored directly on User model', + since: 'v1.0.0', + alternative: 'User model fields (username, service_type, etc.)' + )] public function networkUser(): HasOne { return $this->hasOne(NetworkUser::class, 'user_id'); diff --git a/tests/Unit/Attributes/DeprecatedTest.php b/tests/Unit/Attributes/DeprecatedTest.php new file mode 100644 index 00000000..2e425b07 --- /dev/null +++ b/tests/Unit/Attributes/DeprecatedTest.php @@ -0,0 +1,228 @@ +assertSame('DEP-2024-001', $deprecated->guid); + $this->assertSame('This method is deprecated', $deprecated->message); + $this->assertSame('v1.0.0', $deprecated->since); + $this->assertSame('NewClass::newMethod()', $deprecated->alternative); + $this->assertSame('v2.0.0', $deprecated->removeIn); + } + + public function test_can_create_deprecated_attribute_with_minimal_parameters(): void + { + $deprecated = new Deprecated( + guid: 'DEP-2024-002', + message: 'This is deprecated' + ); + + $this->assertSame('DEP-2024-002', $deprecated->guid); + $this->assertSame('This is deprecated', $deprecated->message); + $this->assertNull($deprecated->since); + $this->assertNull($deprecated->alternative); + $this->assertNull($deprecated->removeIn); + } + + public function test_formatted_message_includes_all_information(): void + { + $deprecated = new Deprecated( + guid: 'DEP-2024-003', + message: 'Method is no longer supported', + since: 'v1.5.0', + alternative: 'BetterClass::betterMethod()', + removeIn: 'v3.0.0' + ); + + $expected = 'DEPRECATED [DEP-2024-003] since v1.5.0 - Method is no longer supported Use BetterClass::betterMethod() instead. Will be removed in v3.0.0.'; + $this->assertSame($expected, $deprecated->getFormattedMessage()); + } + + public function test_formatted_message_with_minimal_info(): void + { + $deprecated = new Deprecated( + guid: 'DEP-2024-004', + message: 'Deprecated functionality' + ); + + $expected = 'DEPRECATED [DEP-2024-004] - Deprecated functionality'; + $this->assertSame($expected, $deprecated->getFormattedMessage()); + } + + public function test_formatted_message_with_since_only(): void + { + $deprecated = new Deprecated( + guid: 'DEP-2024-005', + message: 'Old method', + since: 'v2.0.0' + ); + + $expected = 'DEPRECATED [DEP-2024-005] since v2.0.0 - Old method'; + $this->assertSame($expected, $deprecated->getFormattedMessage()); + } + + public function test_formatted_message_with_alternative_only(): void + { + $deprecated = new Deprecated( + guid: 'DEP-2024-006', + message: 'Use new API', + alternative: 'NewApi::call()' + ); + + $expected = 'DEPRECATED [DEP-2024-006] - Use new API Use NewApi::call() instead.'; + $this->assertSame($expected, $deprecated->getFormattedMessage()); + } + + public function test_to_array_returns_all_properties(): void + { + $deprecated = new Deprecated( + guid: 'DEP-2024-007', + message: 'Test message', + since: 'v1.0.0', + alternative: 'TestClass::test()', + removeIn: 'v2.0.0' + ); + + $array = $deprecated->toArray(); + + $this->assertArrayHasKey('guid', $array); + $this->assertArrayHasKey('message', $array); + $this->assertArrayHasKey('since', $array); + $this->assertArrayHasKey('alternative', $array); + $this->assertArrayHasKey('removeIn', $array); + $this->assertArrayHasKey('formattedMessage', $array); + + $this->assertSame('DEP-2024-007', $array['guid']); + $this->assertSame('Test message', $array['message']); + $this->assertSame('v1.0.0', $array['since']); + $this->assertSame('TestClass::test()', $array['alternative']); + $this->assertSame('v2.0.0', $array['removeIn']); + $this->assertIsString($array['formattedMessage']); + } + + public function test_attribute_can_be_applied_to_method(): void + { + $reflection = new ReflectionMethod(DeprecatedTestSubject::class, 'deprecatedMethod'); + $attributes = $reflection->getAttributes(Deprecated::class); + + $this->assertCount(1, $attributes); + + /** @var Deprecated $deprecated */ + $deprecated = $attributes[0]->newInstance(); + + $this->assertSame('DEP-TEST-001', $deprecated->guid); + $this->assertSame('This method is deprecated', $deprecated->message); + } + + public function test_attribute_can_be_applied_to_class(): void + { + $reflection = new ReflectionClass(DeprecatedTestClass::class); + $attributes = $reflection->getAttributes(Deprecated::class); + + $this->assertCount(1, $attributes); + + /** @var Deprecated $deprecated */ + $deprecated = $attributes[0]->newInstance(); + + $this->assertSame('DEP-TEST-002', $deprecated->guid); + $this->assertSame('This class is deprecated', $deprecated->message); + } + + public function test_attribute_can_be_applied_to_property(): void + { + $reflection = new ReflectionClass(DeprecatedTestSubject::class); + $property = $reflection->getProperty('deprecatedProperty'); + $attributes = $property->getAttributes(Deprecated::class); + + $this->assertCount(1, $attributes); + + /** @var Deprecated $deprecated */ + $deprecated = $attributes[0]->newInstance(); + + $this->assertSame('DEP-TEST-003', $deprecated->guid); + } + + public function test_attribute_can_be_applied_to_constant(): void + { + $reflection = new ReflectionClass(DeprecatedTestSubject::class); + $constant = $reflection->getReflectionConstant('DEPRECATED_CONSTANT'); + $attributes = $constant->getAttributes(Deprecated::class); + + $this->assertCount(1, $attributes); + + /** @var Deprecated $deprecated */ + $deprecated = $attributes[0]->newInstance(); + + $this->assertSame('DEP-TEST-004', $deprecated->guid); + } +} + +/** + * Test subject class with deprecated elements + */ +class DeprecatedTestSubject +{ + #[Deprecated( + guid: 'DEP-TEST-003', + message: 'Property is deprecated' + )] + public string $deprecatedProperty = 'old'; + + #[Deprecated( + guid: 'DEP-TEST-004', + message: 'Constant is deprecated' + )] + public const DEPRECATED_CONSTANT = 'old_value'; + + #[Deprecated( + guid: 'DEP-TEST-001', + message: 'This method is deprecated', + since: 'v1.0.0', + alternative: 'newMethod()', + removeIn: 'v2.0.0' + )] + public function deprecatedMethod(): void + { + // deprecated implementation + } + + public function newMethod(): void + { + // new implementation + } +} + +/** + * Test class marked as deprecated + */ +#[Deprecated( + guid: 'DEP-TEST-002', + message: 'This class is deprecated', + since: 'v1.0.0', + alternative: 'NewTestClass' +)] +class DeprecatedTestClass +{ + public function someMethod(): void + { + // implementation + } +}