467 lines
8.8 KiB
Markdown
467 lines
8.8 KiB
Markdown
|
|
# Contributing to Core PHP Framework
|
||
|
|
|
||
|
|
Thank you for considering contributing to Core PHP Framework! This guide will help you get started.
|
||
|
|
|
||
|
|
## Code of Conduct
|
||
|
|
|
||
|
|
Be respectful, professional, and constructive. We're building open-source software together.
|
||
|
|
|
||
|
|
## How to Contribute
|
||
|
|
|
||
|
|
### Reporting Bugs
|
||
|
|
|
||
|
|
Before creating a bug report:
|
||
|
|
- Check existing issues to avoid duplicates
|
||
|
|
- Verify the bug exists in the latest version
|
||
|
|
- Collect relevant information (PHP version, Laravel version, error messages)
|
||
|
|
|
||
|
|
**Good Bug Report:**
|
||
|
|
|
||
|
|
```markdown
|
||
|
|
**Description:** API key validation fails with bcrypt-hashed keys
|
||
|
|
|
||
|
|
**Steps to Reproduce:**
|
||
|
|
1. Create API key: `$key = ApiKey::create(['name' => 'Test'])`
|
||
|
|
2. Attempt authentication: `GET /api/v1/posts` with key
|
||
|
|
3. Receive 401 Unauthorized
|
||
|
|
|
||
|
|
**Expected:** Authentication succeeds
|
||
|
|
**Actual:** Authentication fails
|
||
|
|
**Version:** v1.0.0
|
||
|
|
**PHP:** 8.2.0
|
||
|
|
**Laravel:** 11.x
|
||
|
|
```
|
||
|
|
|
||
|
|
### Suggesting Features
|
||
|
|
|
||
|
|
Feature requests should include:
|
||
|
|
- Clear use case
|
||
|
|
- Example implementation (if possible)
|
||
|
|
- Impact on existing functionality
|
||
|
|
- Alternative approaches considered
|
||
|
|
|
||
|
|
### Pull Requests
|
||
|
|
|
||
|
|
1. **Fork the repository**
|
||
|
|
2. **Create a feature branch:** `git checkout -b feature/my-feature`
|
||
|
|
3. **Make your changes** (see coding standards below)
|
||
|
|
4. **Write tests** for your changes
|
||
|
|
5. **Run test suite:** `composer test`
|
||
|
|
6. **Commit with clear message:** `feat: add API key rotation`
|
||
|
|
7. **Push to your fork**
|
||
|
|
8. **Open pull request** against `main` branch
|
||
|
|
|
||
|
|
## Development Setup
|
||
|
|
|
||
|
|
### Prerequisites
|
||
|
|
|
||
|
|
- PHP 8.2+
|
||
|
|
- Composer
|
||
|
|
- MySQL/PostgreSQL
|
||
|
|
- Redis (optional, for cache testing)
|
||
|
|
|
||
|
|
### Installation
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Clone your fork
|
||
|
|
git clone https://github.com/YOUR_USERNAME/core-php.git
|
||
|
|
cd core-php
|
||
|
|
|
||
|
|
# Install dependencies
|
||
|
|
composer install
|
||
|
|
|
||
|
|
# Copy environment file
|
||
|
|
cp .env.example .env
|
||
|
|
|
||
|
|
# Run tests
|
||
|
|
composer test
|
||
|
|
```
|
||
|
|
|
||
|
|
### Running Tests
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# All tests
|
||
|
|
composer test
|
||
|
|
|
||
|
|
# Specific test file
|
||
|
|
./vendor/bin/phpunit packages/core-php/tests/Feature/ActivityLogServiceTest.php
|
||
|
|
|
||
|
|
# With coverage
|
||
|
|
./vendor/bin/phpunit --coverage-html coverage
|
||
|
|
```
|
||
|
|
|
||
|
|
## Coding Standards
|
||
|
|
|
||
|
|
### PSR-12 Compliance
|
||
|
|
|
||
|
|
Follow [PSR-12](https://www.php-fig.org/psr/psr-12/) coding standards:
|
||
|
|
|
||
|
|
```php
|
||
|
|
<?php
|
||
|
|
|
||
|
|
namespace Mod\Blog\Actions;
|
||
|
|
|
||
|
|
use Core\Actions\Action;
|
||
|
|
|
||
|
|
class CreatePost
|
||
|
|
{
|
||
|
|
use Action;
|
||
|
|
|
||
|
|
public function handle(array $data): Post
|
||
|
|
{
|
||
|
|
return Post::create($data);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Type Hints
|
||
|
|
|
||
|
|
Always use type hints:
|
||
|
|
|
||
|
|
```php
|
||
|
|
// ✅ Good
|
||
|
|
public function store(Request $request): JsonResponse
|
||
|
|
{
|
||
|
|
$post = CreatePost::run($request->validated());
|
||
|
|
return response()->json($post, 201);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ❌ Bad
|
||
|
|
public function store($request)
|
||
|
|
{
|
||
|
|
$post = CreatePost::run($request->validated());
|
||
|
|
return response()->json($post, 201);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Docblocks
|
||
|
|
|
||
|
|
Use docblocks for complex methods:
|
||
|
|
|
||
|
|
```php
|
||
|
|
/**
|
||
|
|
* Generate OG image for blog post.
|
||
|
|
*
|
||
|
|
* @param Post $post The blog post
|
||
|
|
* @param array $options Image generation options
|
||
|
|
* @return string Path to generated image
|
||
|
|
* @throws ImageGenerationException
|
||
|
|
*/
|
||
|
|
public function generateOgImage(Post $post, array $options = []): string
|
||
|
|
{
|
||
|
|
// Implementation
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Naming Conventions
|
||
|
|
|
||
|
|
**Classes:**
|
||
|
|
- PascalCase
|
||
|
|
- Descriptive names
|
||
|
|
- Singular nouns for models
|
||
|
|
|
||
|
|
```php
|
||
|
|
class Post extends Model {}
|
||
|
|
class CreatePost extends Action {}
|
||
|
|
class PostController extends Controller {}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Methods:**
|
||
|
|
- camelCase
|
||
|
|
- Verb-based names
|
||
|
|
- Descriptive intent
|
||
|
|
|
||
|
|
```php
|
||
|
|
public function createPost() {}
|
||
|
|
public function publishPost() {}
|
||
|
|
public function getPublishedPosts() {}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Variables:**
|
||
|
|
- camelCase
|
||
|
|
- Descriptive names
|
||
|
|
- No abbreviations
|
||
|
|
|
||
|
|
```php
|
||
|
|
// ✅ Good
|
||
|
|
$publishedPosts = Post::published()->get();
|
||
|
|
$userWorkspace = $user->workspace;
|
||
|
|
|
||
|
|
// ❌ Bad
|
||
|
|
$p = Post::published()->get();
|
||
|
|
$ws = $user->workspace;
|
||
|
|
```
|
||
|
|
|
||
|
|
## Module Structure
|
||
|
|
|
||
|
|
Follow the established module pattern:
|
||
|
|
|
||
|
|
```
|
||
|
|
src/Mod/MyModule/
|
||
|
|
├── Boot.php # Module entry point
|
||
|
|
├── Controllers/
|
||
|
|
│ ├── Web/
|
||
|
|
│ └── Api/
|
||
|
|
├── Models/
|
||
|
|
├── Actions/
|
||
|
|
├── Migrations/
|
||
|
|
├── Routes/
|
||
|
|
│ ├── web.php
|
||
|
|
│ └── api.php
|
||
|
|
├── Views/
|
||
|
|
│ └── Blade/
|
||
|
|
├── Lang/
|
||
|
|
│ └── en_GB/
|
||
|
|
└── Tests/
|
||
|
|
├── Feature/
|
||
|
|
└── Unit/
|
||
|
|
```
|
||
|
|
|
||
|
|
**Boot.php Example:**
|
||
|
|
|
||
|
|
```php
|
||
|
|
<?php
|
||
|
|
|
||
|
|
namespace Mod\MyModule;
|
||
|
|
|
||
|
|
use Core\Events\WebRoutesRegistering;
|
||
|
|
use Core\Events\AdminPanelBooting;
|
||
|
|
|
||
|
|
class Boot
|
||
|
|
{
|
||
|
|
public static array $listens = [
|
||
|
|
WebRoutesRegistering::class => 'onWebRoutes',
|
||
|
|
AdminPanelBooting::class => 'onAdminPanel',
|
||
|
|
];
|
||
|
|
|
||
|
|
public function onWebRoutes(WebRoutesRegistering $event): void
|
||
|
|
{
|
||
|
|
$event->views('mymodule', __DIR__.'/Views');
|
||
|
|
$event->routes(fn () => require __DIR__.'/Routes/web.php');
|
||
|
|
}
|
||
|
|
|
||
|
|
public function onAdminPanel(AdminPanelBooting $event): void
|
||
|
|
{
|
||
|
|
$event->menu(new MyModuleMenuProvider());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Testing Guidelines
|
||
|
|
|
||
|
|
### Write Tests First
|
||
|
|
|
||
|
|
Follow TDD when possible:
|
||
|
|
|
||
|
|
```php
|
||
|
|
// 1. Write test
|
||
|
|
public function test_creates_post(): void
|
||
|
|
{
|
||
|
|
$post = CreatePost::run([
|
||
|
|
'title' => 'Test Post',
|
||
|
|
'content' => 'Content',
|
||
|
|
]);
|
||
|
|
|
||
|
|
$this->assertDatabaseHas('posts', [
|
||
|
|
'title' => 'Test Post',
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
|
||
|
|
// 2. Implement feature
|
||
|
|
class CreatePost
|
||
|
|
{
|
||
|
|
use Action;
|
||
|
|
|
||
|
|
public function handle(array $data): Post
|
||
|
|
{
|
||
|
|
return Post::create($data);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Test Coverage
|
||
|
|
|
||
|
|
Aim for 80%+ coverage on new code:
|
||
|
|
|
||
|
|
```bash
|
||
|
|
./vendor/bin/phpunit --coverage-text
|
||
|
|
```
|
||
|
|
|
||
|
|
### Test Organization
|
||
|
|
|
||
|
|
```php
|
||
|
|
class PostTest extends TestCase
|
||
|
|
{
|
||
|
|
// Feature tests - test complete workflows
|
||
|
|
public function test_user_can_create_post(): void {}
|
||
|
|
public function test_user_cannot_create_post_without_permission(): void {}
|
||
|
|
|
||
|
|
// Unit tests - test isolated components
|
||
|
|
public function test_post_is_published(): void {}
|
||
|
|
public function test_post_has_slug(): void {}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Commit Messages
|
||
|
|
|
||
|
|
Follow [Conventional Commits](https://www.conventionalcommits.org/):
|
||
|
|
|
||
|
|
```
|
||
|
|
<type>(<scope>): <subject>
|
||
|
|
|
||
|
|
<body>
|
||
|
|
|
||
|
|
<footer>
|
||
|
|
```
|
||
|
|
|
||
|
|
**Types:**
|
||
|
|
- `feat:` New feature
|
||
|
|
- `fix:` Bug fix
|
||
|
|
- `docs:` Documentation only
|
||
|
|
- `refactor:` Code refactoring
|
||
|
|
- `test:` Adding tests
|
||
|
|
- `chore:` Maintenance tasks
|
||
|
|
|
||
|
|
**Examples:**
|
||
|
|
|
||
|
|
```
|
||
|
|
feat(api): add API key rotation endpoint
|
||
|
|
|
||
|
|
Implements automatic API key rotation with configurable expiry.
|
||
|
|
Keys are hashed with bcrypt for security.
|
||
|
|
|
||
|
|
Closes #123
|
||
|
|
```
|
||
|
|
|
||
|
|
```
|
||
|
|
fix(mcp): validate workspace context in query tool
|
||
|
|
|
||
|
|
Previously, queries could bypass workspace scoping if context
|
||
|
|
was not explicitly validated.
|
||
|
|
|
||
|
|
BREAKING CHANGE: Query tool now requires workspace context
|
||
|
|
```
|
||
|
|
|
||
|
|
## Documentation
|
||
|
|
|
||
|
|
### Code Comments
|
||
|
|
|
||
|
|
Comment why, not what:
|
||
|
|
|
||
|
|
```php
|
||
|
|
// ✅ Good
|
||
|
|
// Hash IP for GDPR compliance
|
||
|
|
$properties['ip_address'] = LthnHash::make(request()->ip());
|
||
|
|
|
||
|
|
// ❌ Bad
|
||
|
|
// Hash the IP address
|
||
|
|
$properties['ip_address'] = LthnHash::make(request()->ip());
|
||
|
|
```
|
||
|
|
|
||
|
|
### README Updates
|
||
|
|
|
||
|
|
Update relevant README files when:
|
||
|
|
- Adding new features
|
||
|
|
- Changing configuration
|
||
|
|
- Modifying installation steps
|
||
|
|
|
||
|
|
### VitePress Documentation
|
||
|
|
|
||
|
|
Add documentation for new features:
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Create new doc page
|
||
|
|
docs/packages/my-package/my-feature.md
|
||
|
|
|
||
|
|
# Update config
|
||
|
|
docs/.vitepress/config.js
|
||
|
|
```
|
||
|
|
|
||
|
|
## Security
|
||
|
|
|
||
|
|
### Reporting Vulnerabilities
|
||
|
|
|
||
|
|
**Do not** open public issues for security vulnerabilities.
|
||
|
|
|
||
|
|
Email: [security@host.uk](mailto:security@host.uk)
|
||
|
|
|
||
|
|
Include:
|
||
|
|
- Description of vulnerability
|
||
|
|
- Steps to reproduce
|
||
|
|
- Potential impact
|
||
|
|
- Suggested fix (if any)
|
||
|
|
|
||
|
|
### Security Best Practices
|
||
|
|
|
||
|
|
```php
|
||
|
|
// ✅ Good - parameterized query
|
||
|
|
$posts = DB::select('SELECT * FROM posts WHERE id = ?', [$id]);
|
||
|
|
|
||
|
|
// ❌ Bad - SQL injection risk
|
||
|
|
$posts = DB::select("SELECT * FROM posts WHERE id = {$id}");
|
||
|
|
|
||
|
|
// ✅ Good - workspace scoping
|
||
|
|
$post = Post::where('workspace_id', $workspace->id)->find($id);
|
||
|
|
|
||
|
|
// ❌ Bad - potential data leak
|
||
|
|
$post = Post::find($id);
|
||
|
|
```
|
||
|
|
|
||
|
|
## Performance
|
||
|
|
|
||
|
|
### Database Queries
|
||
|
|
|
||
|
|
```php
|
||
|
|
// ✅ Good - eager loading
|
||
|
|
$posts = Post::with(['author', 'category'])->get();
|
||
|
|
|
||
|
|
// ❌ Bad - N+1 queries
|
||
|
|
$posts = Post::all();
|
||
|
|
foreach ($posts as $post) {
|
||
|
|
echo $post->author->name; // Query per post
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Caching
|
||
|
|
|
||
|
|
```php
|
||
|
|
// ✅ Good - cache expensive operations
|
||
|
|
$stats = Cache::remember('workspace.stats', 3600, function () {
|
||
|
|
return $this->calculateStats();
|
||
|
|
});
|
||
|
|
|
||
|
|
// ❌ Bad - no caching
|
||
|
|
$stats = $this->calculateStats(); // Slow query
|
||
|
|
```
|
||
|
|
|
||
|
|
## Review Process
|
||
|
|
|
||
|
|
### What We Look For
|
||
|
|
|
||
|
|
- Code follows PSR-12 standards
|
||
|
|
- Tests are included and passing
|
||
|
|
- Documentation is updated
|
||
|
|
- No security vulnerabilities
|
||
|
|
- Performance is acceptable
|
||
|
|
- Backward compatibility maintained
|
||
|
|
|
||
|
|
### CI Checks
|
||
|
|
|
||
|
|
Pull requests must pass:
|
||
|
|
- PHPUnit tests
|
||
|
|
- PHPStan static analysis (level 5)
|
||
|
|
- PHP CS Fixer
|
||
|
|
- Security audit
|
||
|
|
|
||
|
|
## License
|
||
|
|
|
||
|
|
By contributing, you agree that your contributions will be licensed under the EUPL-1.2 License.
|
||
|
|
|
||
|
|
## Questions?
|
||
|
|
|
||
|
|
- Open a [Discussion](https://github.com/host-uk/core-php/discussions)
|
||
|
|
- Join our [Discord](https://discord.gg/host-uk)
|
||
|
|
- Read the [Documentation](https://host-uk.github.io/core-php/)
|
||
|
|
|
||
|
|
Thank you for contributing! 🎉
|