Files
Jonathan Miller cbfa23c4c4
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Scripts governance (push) Successful in 5s
Generic: Repo Health / Release configuration (push) Successful in 5s
Generic: Repo Health / Repository health (push) Successful in 12s
Platform: moko-platform CI / Gate 1: Code Quality (push) Successful in 45s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 4s
Universal: PR Check / Validate PR (pull_request) Successful in 5s
Universal: PR Check / Build RC Package (pull_request) Successful in 2s
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Failing after 44s
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Failing after 48s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Failing after 48s
Platform: moko-platform CI / Gate 4: Governance (push) Successful in 48s
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Failing after 50s
Platform: moko-platform CI / Gate 5: Template Integrity (push) Failing after 12s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Successful in 1m13s
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Failing after 5s
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Failing after 42s
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Failing after 45s
Platform: moko-platform CI / Gate 4: Governance (pull_request) Successful in 44s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Failing after 47s
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Failing after 49s
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Platform: moko-platform CI / CI Summary (pull_request) Has been cancelled
fix: PHPStan level 0 → 2 — fix 67 type errors across 18 files
Real bugs found and fixed:
- bulk_joomla_template: $org undefined in heredoc (missing parameter)
- RepositorySynchronizer: $root undefined (should be $repoRoot), duplicate array key
- RepositoryHealthChecker: wrong class name (UnifiedValidation → UnifiedValidator)
- scan_drift: missing $adapter property declaration
- auto_detect_platform: wrong method name (detectProjectType → detect)
- EnterpriseReadinessValidator: void return used as value
- check_client_theme: extra parameter to printSummary()
- ApiClient: unused constructor parameter now stored
- GitPlatformAdapter: added listBranches/getCloneUrl/cloneRepo to interface
- MokoGiteaAdapter/GitHubAdapter: implemented new interface methods

3 legacy CLIApp scripts excluded (need migration to CliFramework):
  repo_cleanup.php, push_files.php, joomla_release.php

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-25 19:29:52 -05:00

839 lines
25 KiB
PHP

<?php
/**
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: MokoStandards.Enterprise.Plugins
* INGROUP: MokoStandards
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /lib/Enterprise/Plugins/ApiPlugin.php
* BRIEF: Enterprise plugin for API/Microservices projects
*/
declare(strict_types=1);
namespace MokoEnterprise\Plugins;
use MokoEnterprise\AbstractProjectPlugin;
/**
* API/Microservices Project Plugin
*
* Provides validation, metrics, and management capabilities for
* API and microservices projects (REST, GraphQL, gRPC).
*/
class ApiPlugin extends AbstractProjectPlugin
{
/**
* {@inheritdoc}
*/
public function getProjectType(): string
{
return 'api';
}
/**
* {@inheritdoc}
*/
public function getPluginName(): string
{
return 'API/Microservices Enterprise Plugin';
}
/**
* {@inheritdoc}
*/
public function validateProject(array $config, string $projectPath): array
{
$errors = [];
$warnings = [];
$apiType = $this->detectAPIType($projectPath);
// Check for API documentation
if (!$this->hasAPIDocumentation($projectPath, $apiType)) {
$errors[] = 'No API documentation found (OpenAPI, GraphQL schema, etc.)';
}
// Check for proper error handling
if (!$this->hasErrorHandling($projectPath)) {
$warnings[] = 'Consider implementing standardized error handling';
}
// Check for authentication
if (!$this->hasAuthentication($projectPath)) {
$warnings[] = 'No authentication mechanism detected';
}
// Check for rate limiting
if (!$this->hasRateLimiting($projectPath)) {
$warnings[] = 'Consider implementing rate limiting';
}
// Check for logging
if (!$this->hasLogging($projectPath)) {
$warnings[] = 'No logging configuration found';
}
// Check for input validation
if (!$this->hasInputValidation($projectPath)) {
$warnings[] = 'Ensure proper input validation is implemented';
}
// Check for CORS configuration
if (!$this->hasCORSConfig($projectPath)) {
$warnings[] = 'No CORS configuration found';
}
// Check for tests
if (!$this->hasTests($projectPath)) {
$warnings[] = 'No API tests found';
}
$this->log(
'API project validation completed',
'info',
['errors' => count($errors), 'warnings' => count($warnings), 'type' => $apiType]
);
return [
'valid' => empty($errors),
'errors' => $errors,
'warnings' => $warnings,
];
}
/**
* {@inheritdoc}
*/
public function collectMetrics(string $projectPath, array $config): array
{
$apiType = $this->detectAPIType($projectPath);
$language = $this->detectLanguage($projectPath);
$metrics = [
'api_type' => $apiType,
'language' => $language,
'has_documentation' => $this->hasAPIDocumentation($projectPath, $apiType),
'has_authentication' => $this->hasAuthentication($projectPath),
'has_authorization' => $this->hasAuthorization($projectPath),
'has_rate_limiting' => $this->hasRateLimiting($projectPath),
'has_logging' => $this->hasLogging($projectPath),
'has_monitoring' => $this->hasMonitoring($projectPath),
'has_caching' => $this->hasCaching($projectPath),
'has_tests' => $this->hasTests($projectPath),
'has_docker' => $this->fileExists($projectPath, 'Dockerfile'),
'has_ci' => $this->hasCICD($projectPath),
'has_kubernetes' => $this->hasKubernetes($projectPath),
];
// Count endpoints
$metrics['endpoints_count'] = $this->countEndpoints($projectPath, $apiType, $language);
// Count routes/controllers
$metrics['routes_count'] = $this->countRoutes($projectPath, $language);
// Count middleware
$metrics['middleware_count'] = $this->countMiddleware($projectPath, $language);
// Count lines of code
$metrics['total_lines'] = $this->countTotalLines($projectPath, $language);
// Detect framework
$metrics['framework'] = $this->detectFramework($projectPath, $language);
// Record metrics
$this->recordMetric('api', 'endpoints', $metrics['endpoints_count']);
$this->recordMetric('api', 'total_lines', $metrics['total_lines']);
$this->log('Collected API metrics', 'info', $metrics);
return $metrics;
}
/**
* {@inheritdoc}
*/
public function healthCheck(string $projectPath, array $config): array
{
$issues = [];
$score = 100;
$apiType = $this->detectAPIType($projectPath);
// Check for API documentation
if (!$this->hasAPIDocumentation($projectPath, $apiType)) {
$issues[] = [
'severity' => 'warning',
'message' => 'Missing API documentation',
];
$score -= 15;
}
// Check for authentication
if (!$this->hasAuthentication($projectPath)) {
$issues[] = [
'severity' => 'critical',
'message' => 'No authentication mechanism detected',
];
$score -= 20;
}
// Check for authorization
if (!$this->hasAuthorization($projectPath)) {
$issues[] = [
'severity' => 'warning',
'message' => 'No authorization/access control detected',
];
$score -= 10;
}
// Check for input validation
if (!$this->hasInputValidation($projectPath)) {
$issues[] = [
'severity' => 'critical',
'message' => 'Input validation may be missing',
];
$score -= 20;
}
// Check for rate limiting
if (!$this->hasRateLimiting($projectPath)) {
$issues[] = [
'severity' => 'warning',
'message' => 'No rate limiting configured',
];
$score -= 10;
}
// Check for logging
if (!$this->hasLogging($projectPath)) {
$issues[] = [
'severity' => 'warning',
'message' => 'No logging configuration found',
];
$score -= 10;
}
// Check for tests
if (!$this->hasTests($projectPath)) {
$issues[] = [
'severity' => 'warning',
'message' => 'No API tests found',
];
$score -= 15;
}
// Check for README
if (!$this->fileExists($projectPath, 'README.md')) {
$issues[] = [
'severity' => 'warning',
'message' => 'Missing README.md',
];
$score -= 5;
}
// Check for environment configuration
if (!$this->fileExists($projectPath, '.env.example')) {
$issues[] = [
'severity' => 'info',
'message' => 'Missing .env.example for configuration',
];
$score -= 5;
}
$score = max(0, $score);
$this->log('API health check completed', 'info', [
'score' => $score,
'issues_count' => count($issues),
'api_type' => $apiType,
]);
return [
'healthy' => $score >= 70,
'score' => $score,
'issues' => $issues,
];
}
/**
* {@inheritdoc}
*/
public function getRequiredFiles(): array
{
return [
'API documentation (openapi.yaml, swagger.json, schema.graphql)',
'Authentication configuration',
'Error handling middleware',
];
}
/**
* {@inheritdoc}
*/
public function getRecommendedFiles(): array
{
return [
'README.md',
'.env.example',
'openapi.yaml or swagger.json (REST)',
'schema.graphql (GraphQL)',
'Dockerfile',
'docker-compose.yml',
'kubernetes/*.yaml',
'tests/ or test/',
'.mokogitea/workflows/* or .gitea/workflows/* or .gitlab-ci.yml',
'middleware/ or middlewares/',
];
}
/**
* {@inheritdoc}
*/
public function getConfigSchema(): array
{
return [
'type' => 'object',
'properties' => [
'api_type' => [
'type' => 'string',
'enum' => ['rest', 'graphql', 'grpc', 'soap', 'websocket'],
'description' => 'API type',
],
'authentication' => [
'type' => 'string',
'enum' => ['jwt', 'oauth2', 'api-key', 'basic', 'none'],
'description' => 'Authentication method',
],
'framework' => [
'type' => 'string',
'description' => 'Framework used (Express, FastAPI, Spring Boot, etc.)',
],
'enable_rate_limiting' => [
'type' => 'boolean',
'description' => 'Enable rate limiting',
],
'enable_caching' => [
'type' => 'boolean',
'description' => 'Enable response caching',
],
'port' => [
'type' => 'integer',
'description' => 'API server port',
],
],
'required' => ['api_type'],
];
}
/**
* {@inheritdoc}
*/
public function getBestPractices(): array
{
return [
'Document API with OpenAPI/Swagger or GraphQL schema',
'Implement proper authentication (JWT, OAuth2)',
'Use authorization for access control',
'Validate all input data',
'Implement rate limiting to prevent abuse',
'Use standardized error responses',
'Implement comprehensive logging',
'Add monitoring and metrics collection',
'Use HTTPS/TLS for all endpoints',
'Implement CORS properly',
'Version your API endpoints',
'Use pagination for list endpoints',
'Implement caching where appropriate',
'Write comprehensive API tests',
'Use Docker for consistent deployments',
];
}
/**
* Detect API type
*/
private function detectAPIType(string $projectPath): string
{
// GraphQL
if (
$this->fileExists($projectPath, 'schema.graphql') ||
$this->fileExists($projectPath, '*.graphql')
) {
return 'graphql';
}
// gRPC
if ($this->fileExists($projectPath, '*.proto')) {
return 'grpc';
}
// REST (OpenAPI/Swagger)
if (
$this->fileExists($projectPath, 'openapi.yaml') ||
$this->fileExists($projectPath, 'openapi.json') ||
$this->fileExists($projectPath, 'swagger.yaml') ||
$this->fileExists($projectPath, 'swagger.json')
) {
return 'rest';
}
// Check code for REST patterns
$files = $this->findFiles($projectPath, '**/*.{js,ts,py,java,go,php}');
foreach (array_slice($files, 0, 10) as $file) {
if (is_file($file)) {
$content = @file_get_contents($file);
if ($content) {
if (
preg_match('/@(Get|Post|Put|Delete|Patch)\(/', $content) ||
preg_match('/(get|post|put|delete|patch)\s*\([\'"]/', $content)
) {
return 'rest';
}
}
}
}
return 'rest';
}
/**
* Detect language
*/
private function detectLanguage(string $projectPath): string
{
$counts = [
'js' => $this->countFiles($projectPath, '**/*.js'),
'ts' => $this->countFiles($projectPath, '**/*.ts'),
'py' => $this->countFiles($projectPath, '**/*.py'),
'java' => $this->countFiles($projectPath, '**/*.java'),
'go' => $this->countFiles($projectPath, '**/*.go'),
'php' => $this->countFiles($projectPath, '**/*.php'),
];
arsort($counts);
$topLang = array_key_first($counts);
$langMap = [
'js' => 'JavaScript',
'ts' => 'TypeScript',
'py' => 'Python',
'java' => 'Java',
'go' => 'Go',
'php' => 'PHP',
];
return $langMap[$topLang] ?? 'Unknown';
}
/**
* Check for API documentation
*/
private function hasAPIDocumentation(string $projectPath, string $apiType): bool
{
if ($apiType === 'graphql') {
return $this->fileExists($projectPath, 'schema.graphql') ||
$this->countFiles($projectPath, '**/*.graphql') > 0;
}
if ($apiType === 'grpc') {
return $this->countFiles($projectPath, '**/*.proto') > 0;
}
// REST
return $this->fileExists($projectPath, 'openapi.yaml') ||
$this->fileExists($projectPath, 'openapi.json') ||
$this->fileExists($projectPath, 'swagger.yaml') ||
$this->fileExists($projectPath, 'swagger.json');
}
/**
* Check for error handling
*/
private function hasErrorHandling(string $projectPath): bool
{
$files = $this->findFiles($projectPath, '**/*.{js,ts,py}');
foreach (array_slice($files, 0, 10) as $file) {
if (is_file($file)) {
$content = @file_get_contents($file);
if (
$content && (
strpos($content, 'errorHandler') !== false ||
strpos($content, 'error_handler') !== false ||
preg_match('/class\s+\w*Error/', $content)
)
) {
return true;
}
}
}
return false;
}
/**
* Check for authentication
*/
private function hasAuthentication(string $projectPath): bool
{
$files = $this->findFiles($projectPath, '**/*.{js,ts,py,java,go,php}');
foreach (array_slice($files, 0, 15) as $file) {
if (is_file($file)) {
$content = @file_get_contents($file);
if (
$content && (
stripos($content, 'jwt') !== false ||
stripos($content, 'oauth') !== false ||
stripos($content, 'passport') !== false ||
stripos($content, 'authenticate') !== false ||
stripos($content, 'api_key') !== false ||
stripos($content, 'bearer') !== false
)
) {
return true;
}
}
}
return false;
}
/**
* Check for authorization
*/
private function hasAuthorization(string $projectPath): bool
{
$files = $this->findFiles($projectPath, '**/*.{js,ts,py,java,go,php}');
foreach (array_slice($files, 0, 10) as $file) {
if (is_file($file)) {
$content = @file_get_contents($file);
if (
$content && (
stripos($content, 'authorize') !== false ||
stripos($content, 'permission') !== false ||
stripos($content, 'role') !== false ||
stripos($content, 'acl') !== false
)
) {
return true;
}
}
}
return false;
}
/**
* Check for rate limiting
*/
private function hasRateLimiting(string $projectPath): bool
{
$files = $this->findFiles($projectPath, '**/*.{js,ts,py,java,go,php}');
foreach (array_slice($files, 0, 10) as $file) {
if (is_file($file)) {
$content = @file_get_contents($file);
if (
$content && (
stripos($content, 'rate_limit') !== false ||
stripos($content, 'rateLimit') !== false ||
stripos($content, 'throttle') !== false
)
) {
return true;
}
}
}
return false;
}
/**
* Check for logging
*/
private function hasLogging(string $projectPath): bool
{
$files = $this->findFiles($projectPath, '**/*.{js,ts,py,java,go,php}');
foreach (array_slice($files, 0, 10) as $file) {
if (is_file($file)) {
$content = @file_get_contents($file);
if (
$content && (
stripos($content, 'logger') !== false ||
stripos($content, 'winston') !== false ||
stripos($content, 'logging') !== false ||
stripos($content, 'log.') !== false
)
) {
return true;
}
}
}
return false;
}
/**
* Check for monitoring
*/
private function hasMonitoring(string $projectPath): bool
{
$files = $this->findFiles($projectPath, '**/*.{js,ts,py,java,go,php}');
foreach (array_slice($files, 0, 10) as $file) {
if (is_file($file)) {
$content = @file_get_contents($file);
if (
$content && (
stripos($content, 'prometheus') !== false ||
stripos($content, 'metrics') !== false ||
stripos($content, 'monitoring') !== false ||
stripos($content, 'newrelic') !== false
)
) {
return true;
}
}
}
return false;
}
/**
* Check for caching
*/
private function hasCaching(string $projectPath): bool
{
$files = $this->findFiles($projectPath, '**/*.{js,ts,py,java,go,php}');
foreach (array_slice($files, 0, 10) as $file) {
if (is_file($file)) {
$content = @file_get_contents($file);
if (
$content && (
stripos($content, 'redis') !== false ||
stripos($content, 'cache') !== false ||
stripos($content, 'memcached') !== false
)
) {
return true;
}
}
}
return false;
}
/**
* Check for input validation
*/
private function hasInputValidation(string $projectPath): bool
{
$files = $this->findFiles($projectPath, '**/*.{js,ts,py,java,go,php}');
foreach (array_slice($files, 0, 10) as $file) {
if (is_file($file)) {
$content = @file_get_contents($file);
if (
$content && (
stripos($content, 'validate') !== false ||
stripos($content, 'validator') !== false ||
stripos($content, 'joi') !== false ||
stripos($content, 'yup') !== false
)
) {
return true;
}
}
}
return false;
}
/**
* Check for CORS configuration
*/
private function hasCORSConfig(string $projectPath): bool
{
$files = $this->findFiles($projectPath, '**/*.{js,ts,py,java,go,php}');
foreach (array_slice($files, 0, 10) as $file) {
if (is_file($file)) {
$content = @file_get_contents($file);
if ($content && stripos($content, 'cors') !== false) {
return true;
}
}
}
return false;
}
/**
* Check for tests
*/
private function hasTests(string $projectPath): bool
{
return $this->fileExists($projectPath, 'tests') ||
$this->fileExists($projectPath, 'test') ||
$this->fileExists($projectPath, '__tests__') ||
$this->countFiles($projectPath, '**/*.test.*') > 0 ||
$this->countFiles($projectPath, '**/*.spec.*') > 0;
}
/**
* Check for CI/CD
*/
private function hasCICD(string $projectPath): bool
{
return $this->fileExists($projectPath, '.mokogitea/workflows') ||
$this->fileExists($projectPath, '.mokogitea/workflows') ||
$this->fileExists($projectPath, '.gitlab-ci.yml') ||
$this->fileExists($projectPath, 'Jenkinsfile') ||
$this->fileExists($projectPath, '.circleci');
}
/**
* Check for Kubernetes
*/
private function hasKubernetes(string $projectPath): bool
{
return $this->fileExists($projectPath, 'k8s') ||
$this->fileExists($projectPath, 'kubernetes') ||
$this->countFiles($projectPath, '**/*.yaml') > 0;
}
/**
* Count endpoints
*/
private function countEndpoints(string $projectPath, string $apiType, string $language): int
{
$count = 0;
$pattern = $language === 'Python' ? '**/*.py' : '**/*.{js,ts}';
$files = $this->findFiles($projectPath, $pattern);
foreach ($files as $file) {
if (is_file($file)) {
$content = @file_get_contents($file);
if ($content) {
$count += preg_match_all('/@(app\.)?(get|post|put|delete|patch)\s*\(/', $content);
$count += preg_match_all('/\.(get|post|put|delete|patch)\s*\([\'"]/', $content);
}
}
}
return $count;
}
/**
* Count routes
*/
private function countRoutes(string $projectPath, string $language): int
{
$routeFiles = array_merge(
$this->findFiles($projectPath, '**/routes/**/*.{js,ts,py}'),
$this->findFiles($projectPath, '**/route*.{js,ts,py}')
);
return count($routeFiles);
}
/**
* Count middleware
*/
private function countMiddleware(string $projectPath, string $language): int
{
$middlewareFiles = array_merge(
$this->findFiles($projectPath, '**/middleware/**/*.{js,ts,py}'),
$this->findFiles($projectPath, '**/middlewares/**/*.{js,ts,py}')
);
return count($middlewareFiles);
}
/**
* Count total lines
*/
private function countTotalLines(string $projectPath, string $language): int
{
$extMap = [
'JavaScript' => ['js'],
'TypeScript' => ['ts'],
'Python' => ['py'],
'Java' => ['java'],
'Go' => ['go'],
'PHP' => ['php'],
];
$extensions = $extMap[$language] ?? ['js', 'ts', 'py'];
$totalLines = 0;
foreach ($extensions as $ext) {
$files = $this->findFiles($projectPath, "**/*.{$ext}");
foreach ($files as $file) {
if (is_file($file)) {
$totalLines += count(file($file));
}
}
}
return $totalLines;
}
/**
* Detect framework
*/
private function detectFramework(string $projectPath, string $language): string
{
if ($language === 'JavaScript' || $language === 'TypeScript') {
$packageData = $this->parseJsonFile($projectPath, 'package.json');
if ($packageData) {
$deps = array_merge(
$packageData['dependencies'] ?? [],
$packageData['devDependencies'] ?? []
);
if (isset($deps['express'])) {
return 'Express';
}
if (isset($deps['fastify'])) {
return 'Fastify';
}
if (isset($deps['@nestjs/core'])) {
return 'NestJS';
}
if (isset($deps['koa'])) {
return 'Koa';
}
}
}
if ($language === 'Python') {
$requirements = $this->readFile($projectPath, 'requirements.txt');
if ($requirements) {
if (stripos($requirements, 'fastapi') !== false) {
return 'FastAPI';
}
if (stripos($requirements, 'flask') !== false) {
return 'Flask';
}
if (stripos($requirements, 'django') !== false) {
return 'Django';
}
}
}
return 'Unknown';
}
}