diff --git a/composer.json b/composer.json index dde2514..7c01580 100644 --- a/composer.json +++ b/composer.json @@ -36,9 +36,10 @@ "automattic/vipwpcs": "^3.0", "dealerdirect/phpcodesniffer-composer-installer": "^1.0", "phpcompatibility/phpcompatibility-wp": "^2.1", - "phpstan/phpstan": "~2.1", + "phpstan/phpstan": "^1.10", "slevomat/coding-standard": "^8.0", "squizlabs/php_codesniffer": "^3.7", + "szepeviktor/phpstan-wordpress": "^1.3", "wp-coding-standards/wpcs": "^3.0" }, "config": { @@ -58,6 +59,6 @@ ], "phpcs": "phpcs", "phpcbf": "phpcbf", - "phpstan": "phpstan analyze --memory-limit=256M" + "phpstan": "phpstan analyze --memory-limit=1024M" } } diff --git a/composer.lock b/composer.lock index 396adb7..83636d4 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "7dbba8d15fdd8095bb14f9df989589e1", + "content-hash": "f20a0334687aeee1e315f91c06a337f0", "packages": [ { "name": "php-http/discovery", @@ -628,6 +628,57 @@ ], "time": "2025-07-17T20:45:56+00:00" }, + { + "name": "php-stubs/wordpress-stubs", + "version": "v6.8.2", + "source": { + "type": "git", + "url": "https://github.com/php-stubs/wordpress-stubs.git", + "reference": "9c8e22e437463197c1ec0d5eaa9ddd4a0eb6d7f8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/9c8e22e437463197c1ec0d5eaa9ddd4a0eb6d7f8", + "reference": "9c8e22e437463197c1ec0d5eaa9ddd4a0eb6d7f8", + "shasum": "" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "5.6.1" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "nikic/php-parser": "^5.5", + "php": "^7.4 || ^8.0", + "php-stubs/generator": "^0.8.3", + "phpdocumentor/reflection-docblock": "^5.4.1", + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^9.5", + "szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^1.1.1", + "wp-coding-standards/wpcs": "3.1.0 as 2.3.0" + }, + "suggest": { + "paragonie/sodium_compat": "Pure PHP implementation of libsodium", + "symfony/polyfill-php80": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "szepeviktor/phpstan-wordpress": "WordPress extensions for PHPStan" + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "WordPress function and class declaration stubs for static analysis.", + "homepage": "https://github.com/php-stubs/wordpress-stubs", + "keywords": [ + "PHPStan", + "static analysis", + "wordpress" + ], + "support": { + "issues": "https://github.com/php-stubs/wordpress-stubs/issues", + "source": "https://github.com/php-stubs/wordpress-stubs/tree/v6.8.2" + }, + "time": "2025-07-16T06:41:00+00:00" + }, { "name": "phpcompatibility/php-compatibility", "version": "9.3.5", @@ -839,22 +890,22 @@ }, { "name": "phpcsstandards/phpcsextra", - "version": "1.4.0", + "version": "1.4.1", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHPCSExtra.git", - "reference": "fa4b8d051e278072928e32d817456a7fdb57b6ca" + "reference": "882b8c947ada27eb002870fe77fee9ce0a454cdb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/fa4b8d051e278072928e32d817456a7fdb57b6ca", - "reference": "fa4b8d051e278072928e32d817456a7fdb57b6ca", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/882b8c947ada27eb002870fe77fee9ce0a454cdb", + "reference": "882b8c947ada27eb002870fe77fee9ce0a454cdb", "shasum": "" }, "require": { "php": ">=5.4", - "phpcsstandards/phpcsutils": "^1.1.0", - "squizlabs/php_codesniffer": "^3.13.0 || ^4.0" + "phpcsstandards/phpcsutils": "^1.1.2", + "squizlabs/php_codesniffer": "^3.13.4 || ^4.0" }, "require-dev": { "php-parallel-lint/php-console-highlighter": "^1.0", @@ -917,26 +968,26 @@ "type": "thanks_dev" } ], - "time": "2025-06-14T07:40:39+00:00" + "time": "2025-09-05T06:54:52+00:00" }, { "name": "phpcsstandards/phpcsutils", - "version": "1.1.1", + "version": "1.1.2", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHPCSUtils.git", - "reference": "f7eb16f2fa4237d5db9e8fed8050239bee17a9bd" + "reference": "b22b59e3d9ec8fe4953e42c7d59117c6eae70eae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/f7eb16f2fa4237d5db9e8fed8050239bee17a9bd", - "reference": "f7eb16f2fa4237d5db9e8fed8050239bee17a9bd", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/b22b59e3d9ec8fe4953e42c7d59117c6eae70eae", + "reference": "b22b59e3d9ec8fe4953e42c7d59117c6eae70eae", "shasum": "" }, "require": { "dealerdirect/phpcodesniffer-composer-installer": "^0.4.1 || ^0.5 || ^0.6.2 || ^0.7 || ^1.0", "php": ">=5.4", - "squizlabs/php_codesniffer": "^3.13.0 || ^4.0" + "squizlabs/php_codesniffer": "^3.13.3 || ^4.0" }, "require-dev": { "ext-filter": "*", @@ -1010,7 +1061,7 @@ "type": "thanks_dev" } ], - "time": "2025-08-10T01:04:45+00:00" + "time": "2025-09-05T00:00:03+00:00" }, { "name": "phpstan/phpdoc-parser", @@ -1061,20 +1112,20 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.22", + "version": "1.12.28", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "41600c8379eb5aee63e9413fe9e97273e25d57e4" + "reference": "fcf8b71aeab4e1a1131d1783cef97b23a51b87a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/41600c8379eb5aee63e9413fe9e97273e25d57e4", - "reference": "41600c8379eb5aee63e9413fe9e97273e25d57e4", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/fcf8b71aeab4e1a1131d1783cef97b23a51b87a9", + "reference": "fcf8b71aeab4e1a1131d1783cef97b23a51b87a9", "shasum": "" }, "require": { - "php": "^7.4|^8.0" + "php": "^7.2|^8.0" }, "conflict": { "phpstan/phpstan-shim": "*" @@ -1115,7 +1166,7 @@ "type": "github" } ], - "time": "2025-08-04T19:17:37+00:00" + "time": "2025-07-17T17:15:39+00:00" }, { "name": "sirbrillig/phpcs-variable-analysis", @@ -1241,16 +1292,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.13.2", + "version": "3.13.4", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "5b5e3821314f947dd040c70f7992a64eac89025c" + "reference": "ad545ea9c1b7d270ce0fc9cbfb884161cd706119" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/5b5e3821314f947dd040c70f7992a64eac89025c", - "reference": "5b5e3821314f947dd040c70f7992a64eac89025c", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/ad545ea9c1b7d270ce0fc9cbfb884161cd706119", + "reference": "ad545ea9c1b7d270ce0fc9cbfb884161cd706119", "shasum": "" }, "require": { @@ -1321,7 +1372,150 @@ "type": "thanks_dev" } ], - "time": "2025-06-17T22:17:01+00:00" + "time": "2025-09-05T05:47:09+00:00" + }, + { + "name": "symfony/polyfill-php73", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php73.git", + "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/0f68c03565dcaaf25a890667542e8bd75fe7e5bb", + "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php73\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php73/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "szepeviktor/phpstan-wordpress", + "version": "v1.3.5", + "source": { + "type": "git", + "url": "https://github.com/szepeviktor/phpstan-wordpress.git", + "reference": "7f8cfe992faa96b6a33bbd75c7bace98864161e7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/szepeviktor/phpstan-wordpress/zipball/7f8cfe992faa96b6a33bbd75c7bace98864161e7", + "reference": "7f8cfe992faa96b6a33bbd75c7bace98864161e7", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0", + "php-stubs/wordpress-stubs": "^4.7 || ^5.0 || ^6.0", + "phpstan/phpstan": "^1.10.31", + "symfony/polyfill-php73": "^1.12.0" + }, + "require-dev": { + "composer/composer": "^2.1.14", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.1", + "phpstan/phpstan-strict-rules": "^1.2", + "phpunit/phpunit": "^8.0 || ^9.0", + "szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^1.0", + "wp-coding-standards/wpcs": "3.1.0 as 2.3.0" + }, + "suggest": { + "swissspidy/phpstan-no-private": "Detect usage of internal core functions, classes and methods" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + } + }, + "autoload": { + "psr-4": { + "SzepeViktor\\PHPStan\\WordPress\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "WordPress extensions for PHPStan", + "keywords": [ + "PHPStan", + "code analyse", + "code analysis", + "static analysis", + "wordpress" + ], + "support": { + "issues": "https://github.com/szepeviktor/phpstan-wordpress/issues", + "source": "https://github.com/szepeviktor/phpstan-wordpress/tree/v1.3.5" + }, + "time": "2024-06-28T22:27:19+00:00" }, { "name": "wp-coding-standards/wpcs", @@ -1392,14 +1586,14 @@ ], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": { "php": ">=7.4", "ext-json": "*" }, - "platform-dev": [], + "platform-dev": {}, "platform-overrides": { "php": "7.4" }, diff --git a/includes/REST/Controllers/class-prompt-controller.php b/includes/REST/Controllers/class-prompt-controller.php new file mode 100644 index 0000000..5ef9cbe --- /dev/null +++ b/includes/REST/Controllers/class-prompt-controller.php @@ -0,0 +1,409 @@ +namespace = $api_namespace; + } + + /** + * Register routes for PromptBuilder result methods. + * + * @since n.e.x.t + * + * @return void + */ + public function register_routes(): void { + $common_args = $this->get_common_args(); + + // Register routes for result methods. + $routes = array( + '/prompt/generate-result' => 'generate_result', + '/prompt/generate-text-result' => 'generate_text_result', + '/prompt/generate-image-result' => 'generate_image_result', + '/prompt/generate-speech-result' => 'generate_speech_result', + ); + + foreach ( $routes as $route => $callback ) { + register_rest_route( + $this->namespace, + $route, + array( + 'methods' => 'POST', + 'callback' => array( $this, $callback ), + 'args' => $common_args, + 'permission_callback' => array( $this, 'check_permission' ), + 'schema' => array( $this, 'get_response_schema' ), + ) + ); + } + } + + /** + * Get common request arguments for all prompt endpoints. + * + * Leverages schemas from PHP AI Client SDK DTOs to maintain + * consistency with the underlying SDK data structures. + * + * @since n.e.x.t + * + * @return array> Request arguments schema. + */ + protected function get_common_args(): array { + return array( + 'prompt' => array( + 'required' => true, + 'description' => 'The prompt content as string, message part, message, or array of messages', + 'oneOf' => array( + // String prompt. + array( + 'type' => 'string', + 'minLength' => 1, + 'description' => 'Simple text prompt', + ), + // Single MessagePart DTO. + MessagePart::getJsonSchema(), + // Single Message DTO. + Message::getJsonSchema(), + // Array of Message DTOs. + array( + 'type' => 'array', + 'items' => Message::getJsonSchema(), + 'minItems' => 1, + 'description' => 'Array of messages for conversation', + ), + ), + ), + 'config' => array_merge( + ModelConfig::getJsonSchema(), + array( + 'description' => 'Model configuration options', + ) + ), + ); + } + + /** + * Get response schema for all prompt endpoints. + * + * All endpoints return the same response format based on GenerativeAiResult DTO, + * wrapped in a consistent success/error response structure. + * + * @since n.e.x.t + * + * @return array Response schema. + */ + public function get_response_schema(): array { + return array( + 'type' => 'object', + 'properties' => array( + 'success' => array( + 'type' => 'boolean', + 'description' => 'Whether the request was successful', + ), + 'data' => GenerativeAiResult::getJsonSchema(), + 'timestamp' => array( + 'type' => 'string', + 'description' => 'Response timestamp', + ), + 'metadata' => array( + 'type' => 'object', + 'description' => 'Additional response metadata', + 'additionalProperties' => true, + ), + ), + 'required' => array( 'success', 'data', 'timestamp' ), + ); + } + + // ============================================================================= + // RESULT GENERATION METHODS + // ============================================================================= + + /** + * Handle text result generation → PromptBuilder::generateTextResult() + * + * @param WP_REST_Request $request The REST request object. + * @return WP_REST_Response The response object. + */ + public function generate_text_result( WP_REST_Request $request ): WP_REST_Response { + try { + $builder = $this->build_prompt_from_request( $request ); + $result = $builder->generateTextResult(); + + return $this->format_success_response( + $this->format_generative_result( $result ), + array( + 'type' => 'text-result', + 'endpoint' => 'generate-text-result', + ) + ); + } catch ( \Exception $exception ) { + return $this->handle_generation_error( $exception ); + } + } + + /** + * Handle image result generation → PromptBuilder::generateImageResult() + * + * @param WP_REST_Request $request The REST request object. + * @return WP_REST_Response The response object. + */ + public function generate_image_result( WP_REST_Request $request ): WP_REST_Response { + try { + $builder = $this->build_prompt_from_request( $request ); + $result = $builder->generateImageResult(); + + return $this->format_success_response( + $this->format_generative_result( $result ), + array( + 'type' => 'image-result', + 'endpoint' => 'generate-image-result', + ) + ); + } catch ( \Exception $exception ) { + return $this->handle_generation_error( $exception ); + } + } + + /** + * Handle speech result generation → PromptBuilder::generateSpeechResult() + * + * @param WP_REST_Request $request The REST request object. + * @return WP_REST_Response The response object. + */ + public function generate_speech_result( WP_REST_Request $request ): WP_REST_Response { + try { + $builder = $this->build_prompt_from_request( $request ); + $result = $builder->generateSpeechResult(); + + return $this->format_success_response( + $this->format_generative_result( $result ), + array( + 'type' => 'speech-result', + 'endpoint' => 'generate-speech-result', + ) + ); + } catch ( \Exception $exception ) { + return $this->handle_generation_error( $exception ); + } + } + + // ============================================================================= + // GENERAL GENERATION METHOD + // ============================================================================= + + /** + * Handle general result generation → PromptBuilder::generateResult() + * + * @param WP_REST_Request $request The REST request object. + * @return WP_REST_Response The response object. + */ + public function generate_result( WP_REST_Request $request ): WP_REST_Response { + try { + $builder = $this->build_prompt_from_request( $request ); + $result = $builder->generateResult(); + + return $this->format_success_response( + $this->format_generative_result( $result ), + array( + 'type' => 'result', + 'endpoint' => 'generate-result', + ) + ); + } catch ( \Exception $exception ) { + return $this->handle_generation_error( $exception ); + } + } + + // ============================================================================= + // SHARED HELPER METHODS + // ============================================================================= + + /** + * Build a PromptBuilder instance from the REST request. + * + * Uses the config parameter which maps directly to ModelConfig DTO, + * providing a clean interface between REST API and the SDK. + * + * @since n.e.x.t + * + * @param WP_REST_Request $request The REST request object. + * @return PromptBuilder Configured PromptBuilder instance. + */ + protected function build_prompt_from_request( WP_REST_Request $request ) { + // Get prompt content (string, MessagePart, Message, or Message[]). + $prompt = $request->get_param( 'prompt' ); + + // Create builder with prompt - AiClient handles all prompt types. + $builder = AiClient::prompt( $prompt ); + + // Apply configuration if provided - maps directly to ModelConfig DTO. + $config = $request->get_param( 'config' ); + if ( ! empty( $config ) && is_array( $config ) ) { + $model_config = ModelConfig::fromArray( $config ); + $builder->usingModelConfig( $model_config ); + } + + return $builder; + } + + /** + * Format a GenerativeAiResult object for JSON response. + * + * @param GenerativeAiResult $result The generation result. + * @return array Formatted result data. + */ + private function format_generative_result( GenerativeAiResult $result ): array { + $response_data = array( + 'candidates' => array(), + 'finish_reason' => null, + 'created_at' => current_time( 'mysql' ), + ); + + // Add candidates with their content and metadata. + foreach ( $result->getCandidates() as $candidate ) { + // Get message content from candidate. + $message = $candidate->getMessage(); + $parts = $message->getParts(); + + // Extract text content from message parts. + $content = ''; + foreach ( $parts as $part ) { + if ( method_exists( $part, 'getText' ) ) { + $content .= $part->getText(); + } + } + + $candidate_data = array( + 'content' => $content, + 'finish_reason' => $candidate->getFinishReason() ? $candidate->getFinishReason()->value : null, + ); + $response_data['candidates'][] = $candidate_data; + } + + // Set primary finish reason from first candidate. + if ( ! empty( $response_data['candidates'] ) ) { + $response_data['finish_reason'] = $response_data['candidates'][0]['finish_reason']; + } + + // Add token usage information. + $token_usage = $result->getTokenUsage(); + $response_data['token_usage'] = array( + 'prompt_tokens' => $token_usage->getPromptTokens(), + 'completion_tokens' => $token_usage->getCompletionTokens(), + 'total_tokens' => $token_usage->getTotalTokens(), + ); + + return $response_data; + } + + /** + * Format success response with consistent structure. + * + * @param mixed $data Response data. + * @param array $metadata Additional metadata. + * @return WP_REST_Response Success response. + */ + private function format_success_response( $data, array $metadata = array() ): WP_REST_Response { + $response_data = array( + 'success' => true, + 'data' => $data, + 'timestamp' => current_time( 'mysql' ), + 'metadata' => $metadata, + ); + + return new WP_REST_Response( $response_data, 200 ); + } + + /** + * Handle generation errors with consistent error response. + * + * @param \Exception $exception The caught exception. + * @return WP_REST_Response Error response. + */ + private function handle_generation_error( \Exception $exception ): WP_REST_Response { + $error_data = array( + 'success' => false, + 'error' => array( + 'code' => 'generation_failed', + 'message' => $exception->getMessage(), + ), + 'timestamp' => current_time( 'mysql' ), + ); + + return new WP_REST_Response( $error_data, 500 ); + } + + + /** + * Check permissions for AI generation requests. + * + * Only administrators can use AI generation since they configure + * provider credentials and API keys. + * + * @param WP_REST_Request $request The REST request object. + * @return bool|WP_Error True if user has permission, WP_Error if not. + */ + public function check_permission( WP_REST_Request $request ) { + if ( ! current_user_can( 'manage_options' ) ) { + return new WP_Error( + 'rest_forbidden', + 'You do not have permission to use AI generation', + array( 'status' => 403 ) + ); + } + + return true; + } +} diff --git a/includes/REST/class-rest-route-registrar.php b/includes/REST/class-rest-route-registrar.php new file mode 100644 index 0000000..1dd526b --- /dev/null +++ b/includes/REST/class-rest-route-registrar.php @@ -0,0 +1,47 @@ +register_routes(); + } +} diff --git a/includes/Test_Class.php b/includes/Test_Class.php deleted file mode 100644 index ba39831..0000000 --- a/includes/Test_Class.php +++ /dev/null @@ -1,28 +0,0 @@ -