diff --git a/composer.json b/composer.json index dde2514..7d87a6e 100644 --- a/composer.json +++ b/composer.json @@ -30,7 +30,7 @@ "require": { "php": ">=7.4", "ext-json": "*", - "wordpress/php-ai-client": "^0.1" + "wordpress/php-ai-client": "^0.2" }, "require-dev": { "automattic/vipwpcs": "^3.0", @@ -39,7 +39,8 @@ "phpstan/phpstan": "~2.1", "slevomat/coding-standard": "^8.0", "squizlabs/php_codesniffer": "^3.7", - "wp-coding-standards/wpcs": "^3.0" + "wp-coding-standards/wpcs": "^3.0", + "szepeviktor/phpstan-wordpress": "^2.0" }, "config": { "allow-plugins": { @@ -58,6 +59,6 @@ ], "phpcs": "phpcs", "phpcbf": "phpcbf", - "phpstan": "phpstan analyze --memory-limit=256M" + "phpstan": "phpstan analyze --memory-limit=512M" } } diff --git a/composer.lock b/composer.lock index 396adb7..0a4f472 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": "838e75dea18629e0653c626870df6e4a", "packages": [ { "name": "php-http/discovery", @@ -411,16 +411,16 @@ }, { "name": "wordpress/php-ai-client", - "version": "0.1.0", + "version": "0.2.0", "source": { "type": "git", "url": "https://github.com/WordPress/php-ai-client.git", - "reference": "9ec56e70e692791493a3eaff1b69f25f4daeded7" + "reference": "81a104a9bc5f887e3fbecea6e0d9cd8eab3be0b2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/WordPress/php-ai-client/zipball/9ec56e70e692791493a3eaff1b69f25f4daeded7", - "reference": "9ec56e70e692791493a3eaff1b69f25f4daeded7", + "url": "https://api.github.com/repos/WordPress/php-ai-client/zipball/81a104a9bc5f887e3fbecea6e0d9cd8eab3be0b2", + "reference": "81a104a9bc5f887e3fbecea6e0d9cd8eab3be0b2", "shasum": "" }, "require": { @@ -474,7 +474,7 @@ "issues": "https://github.com/WordPress/php-ai-client/issues", "source": "https://github.com/WordPress/php-ai-client" }, - "time": "2025-08-29T22:46:54+00:00" + "time": "2025-10-21T00:05:14+00:00" } ], "packages-dev": [ @@ -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", @@ -1323,6 +1374,69 @@ ], "time": "2025-06-17T22:17:01+00:00" }, + { + "name": "szepeviktor/phpstan-wordpress", + "version": "v2.0.3", + "source": { + "type": "git", + "url": "https://github.com/szepeviktor/phpstan-wordpress.git", + "reference": "aa722f037b2d034828cd6c55ebe9e5c74961927e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/szepeviktor/phpstan-wordpress/zipball/aa722f037b2d034828cd6c55ebe9e5c74961927e", + "reference": "aa722f037b2d034828cd6c55ebe9e5c74961927e", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "php-stubs/wordpress-stubs": "^6.6.2", + "phpstan/phpstan": "^2.0" + }, + "require-dev": { + "composer/composer": "^2.1.14", + "composer/semver": "^3.4", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.1", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^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/v2.0.3" + }, + "time": "2025-09-14T02:58:22+00:00" + }, { "name": "wp-coding-standards/wpcs", "version": "3.2.0", @@ -1392,14 +1506,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/Prompt_Builder.php b/includes/Prompt_Builder.php new file mode 100644 index 0000000..cbba796 --- /dev/null +++ b/includes/Prompt_Builder.php @@ -0,0 +1,146 @@ + $schema) Sets the output schema. + * @method self as_output_modalities(ModalityEnum ...$modalities) Sets the output modalities. + * @method self as_output_file_type(FileTypeEnum $fileType) Sets the output file type. + * @method self as_json_response(?array $schema = null) Configures the prompt for JSON response output. + * @method bool is_supported_for_text_generation() Checks if the prompt is supported for text generation. + * @method bool is_supported_for_image_generation() Checks if the prompt is supported for image generation. + * @method bool is_supported_for_text_to_speech_conversion() Checks if the prompt is supported for text to speech conversion. + * @method bool is_supported_for_video_generation() Checks if the prompt is supported for video generation. + * @method bool is_supported_for_speech_generation() Checks if the prompt is supported for speech generation. + * @method bool is_supported_for_music_generation() Checks if the prompt is supported for music generation. + * @method bool is_supported_for_embedding_generation() Checks if the prompt is supported for embedding generation. + * @method GenerativeAiResult generate_result(?CapabilityEnum $capability = null) Generates a result from the prompt. + * @method GenerativeAiResult generate_text_result() Generates a text result from the prompt. + * @method GenerativeAiResult generate_image_result() Generates an image result from the prompt. + * @method GenerativeAiResult generate_speech_result() Generates a speech result from the prompt. + * @method GenerativeAiResult convert_text_to_speech_result() Converts text to speech and returns the result. + * @method string generate_text() Generates text from the prompt. + * @method list generate_texts(?int $candidateCount = null) Generates multiple text candidates from the prompt. + * @method File generate_image() Generates an image from the prompt. + * @method list generate_images(?int $candidateCount = null) Generates multiple images from the prompt. + * @method File convert_text_to_speech() Converts text to speech. + * @method list convert_text_to_speeches(?int $candidateCount = null) Converts text to multiple speech outputs. + * @method File generate_speech() Generates speech from the prompt. + * @method list generate_speeches(?int $candidateCount = null) Generates multiple speech outputs from the prompt. + */ +class Prompt_Builder extends PromptBuilder { + + /** + * Magic method to handle snake_case method calls. + * + * Converts snake_case method names to camelCase and calls the parent method. + * This allows WordPress developers to use snake_case naming conventions while + * maintaining compatibility with the underlying PHP AI Client library. + * + * @since n.e.x.t + * + * @param string $name The method name in snake_case. + * @param array $arguments The method arguments. + * + * @return mixed The result of the parent method call. + * + * @throws \BadMethodCallException If the method does not exist. + */ + public function __call( string $name, array $arguments ) { + // Convert snake_case to camelCase. + $camel_case_name = $this->snake_to_camel_case( $name ); + + // Check if parent has this method. + if ( ! method_exists( parent::class, $camel_case_name ) ) { + throw new \BadMethodCallException( + sprintf( + 'Method %s does not exist on %s', + $name, // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped + static::class + ) + ); + } + + // Call parent method with the arguments. + // Since the method exists in the parent class, PHP will call it directly. + return $this->$camel_case_name( ...$arguments ); + } + + /** + * Converts snake_case to camelCase. + * + * @since n.e.x.t + * + * @param string $snake_case The snake_case string. + * + * @return string The camelCase string. + */ + private function snake_to_camel_case( string $snake_case ): string { + // Split by underscore. + $parts = explode( '_', $snake_case ); + + // Capitalize first letter of each part except the first. + $camel_case = $parts[0]; + $parts_count = count( $parts ); + for ( $i = 1; $i < $parts_count; $i++ ) { + $camel_case .= ucfirst( $parts[ $i ] ); + } + + return $camel_case; + } +} diff --git a/includes/Prompt_Builder_With_WP_Error.php b/includes/Prompt_Builder_With_WP_Error.php new file mode 100644 index 0000000..ecee61d --- /dev/null +++ b/includes/Prompt_Builder_With_WP_Error.php @@ -0,0 +1,441 @@ +construction_error = new \WP_Error( + 'prompt_builder_invalid_argument', + $e->getMessage(), // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped + array( 'exception' => $e ) + ); + } + } + + /** + * Checks if an error occurred during construction. + * + * @since n.e.x.t + * + * @return bool True if there was a construction error, false otherwise. + */ + public function has_error(): bool { + return null !== $this->construction_error; + } + + /** + * Gets the construction error, if any. + * + * @since n.e.x.t + * + * @return \WP_Error|null The error that occurred during construction, or null. + */ + public function get_error(): ?\WP_Error { + return $this->construction_error; + } + + /** + * Sets preferred models to evaluate in order. + * + * @since n.e.x.t + * + * @param string|ModelInterface|array{0:string,1:string} ...$preferred_models The preferred models. + * + * @return self|\WP_Error This builder instance for chaining, or WP_Error on failure. + */ + public function using_model_preference( ...$preferred_models ) { + try { + parent::usingModelPreference( ...$preferred_models ); + return $this; + } catch ( InvalidArgumentException $e ) { + return new \WP_Error( + 'prompt_builder_invalid_argument', + $e->getMessage(), // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped + array( 'exception' => $e ) + ); + } + } + + /** + * Generates a result from the prompt. + * + * @since n.e.x.t + * + * @param CapabilityEnum|null $capability Optional capability to use for generation. + * + * @return GenerativeAiResult|\WP_Error The generated result, or WP_Error on failure. + */ + public function generate_result( ?CapabilityEnum $capability = null ) { + try { + return parent::generateResult( $capability ); + } catch ( InvalidArgumentException $e ) { + return new \WP_Error( + 'prompt_builder_invalid_argument', + $e->getMessage(), // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped + array( 'exception' => $e ) + ); + } catch ( RuntimeException $e ) { + return new \WP_Error( + 'prompt_builder_runtime_error', + $e->getMessage(), // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped + array( 'exception' => $e ) + ); + } + } + + /** + * Generates a text result from the prompt. + * + * @since n.e.x.t + * + * @return GenerativeAiResult|\WP_Error The generated result, or WP_Error on failure. + */ + public function generate_text_result() { + try { + return parent::generateTextResult(); + } catch ( InvalidArgumentException $e ) { + return new \WP_Error( + 'prompt_builder_invalid_argument', + $e->getMessage(), // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped + array( 'exception' => $e ) + ); + } catch ( RuntimeException $e ) { + return new \WP_Error( + 'prompt_builder_runtime_error', + $e->getMessage(), // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped + array( 'exception' => $e ) + ); + } + } + + /** + * Generates an image result from the prompt. + * + * @since n.e.x.t + * + * @return GenerativeAiResult|\WP_Error The generated result, or WP_Error on failure. + */ + public function generate_image_result() { + try { + return parent::generateImageResult(); + } catch ( InvalidArgumentException $e ) { + return new \WP_Error( + 'prompt_builder_invalid_argument', + $e->getMessage(), // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped + array( 'exception' => $e ) + ); + } catch ( RuntimeException $e ) { + return new \WP_Error( + 'prompt_builder_runtime_error', + $e->getMessage(), // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped + array( 'exception' => $e ) + ); + } + } + + /** + * Generates a speech result from the prompt. + * + * @since n.e.x.t + * + * @return GenerativeAiResult|\WP_Error The generated result, or WP_Error on failure. + */ + public function generate_speech_result() { + try { + return parent::generateSpeechResult(); + } catch ( InvalidArgumentException $e ) { + return new \WP_Error( + 'prompt_builder_invalid_argument', + $e->getMessage(), // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped + array( 'exception' => $e ) + ); + } catch ( RuntimeException $e ) { + return new \WP_Error( + 'prompt_builder_runtime_error', + $e->getMessage(), // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped + array( 'exception' => $e ) + ); + } + } + + /** + * Converts text to speech and returns the result. + * + * @since n.e.x.t + * + * @return GenerativeAiResult|\WP_Error The generated result, or WP_Error on failure. + */ + public function convert_text_to_speech_result() { + try { + return parent::convertTextToSpeechResult(); + } catch ( InvalidArgumentException $e ) { + return new \WP_Error( + 'prompt_builder_invalid_argument', + $e->getMessage(), // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped + array( 'exception' => $e ) + ); + } catch ( RuntimeException $e ) { + return new \WP_Error( + 'prompt_builder_runtime_error', + $e->getMessage(), // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped + array( 'exception' => $e ) + ); + } + } + + /** + * Generates text from the prompt. + * + * @since n.e.x.t + * + * @return string|\WP_Error The generated text, or WP_Error on failure. + */ + public function generate_text() { + try { + return parent::generateText(); + } catch ( InvalidArgumentException | RuntimeException $e ) { // @phpstan-ignore-line catch.neverThrown + // Catch both exception types for consistency. + $code = $e instanceof InvalidArgumentException ? 'prompt_builder_invalid_argument' : 'prompt_builder_runtime_error'; + + return new \WP_Error( + $code, + $e->getMessage(), // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped + array( 'exception' => $e ) + ); + } + } + + /** + * Generates multiple text candidates from the prompt. + * + * @since n.e.x.t + * + * @param int|null $candidate_count The number of candidates to generate. + * + * @return list|\WP_Error The generated texts, or WP_Error on failure. + */ + public function generate_texts( ?int $candidate_count = null ) { + try { + return parent::generateTexts( $candidate_count ); + } catch ( InvalidArgumentException | RuntimeException $e ) { // @phpstan-ignore-line catch.neverThrown + // Catch both exception types for consistency. + $code = $e instanceof InvalidArgumentException ? 'prompt_builder_invalid_argument' : 'prompt_builder_runtime_error'; + + return new \WP_Error( + $code, + $e->getMessage(), // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped + array( 'exception' => $e ) + ); + } + } + + /** + * Generates an image from the prompt. + * + * @since n.e.x.t + * + * @return File|\WP_Error The generated image file, or WP_Error on failure. + */ + public function generate_image() { + try { + return parent::generateImage(); + } catch ( InvalidArgumentException $e ) { + return new \WP_Error( + 'prompt_builder_invalid_argument', + $e->getMessage(), // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped + array( 'exception' => $e ) + ); + } catch ( RuntimeException $e ) { + return new \WP_Error( + 'prompt_builder_runtime_error', + $e->getMessage(), // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped + array( 'exception' => $e ) + ); + } + } + + /** + * Generates multiple images from the prompt. + * + * @since n.e.x.t + * + * @param int|null $candidate_count The number of images to generate. + * + * @return list|\WP_Error The generated image files, or WP_Error on failure. + */ + public function generate_images( ?int $candidate_count = null ) { + try { + return parent::generateImages( $candidate_count ); + } catch ( InvalidArgumentException $e ) { + return new \WP_Error( + 'prompt_builder_invalid_argument', + $e->getMessage(), // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped + array( 'exception' => $e ) + ); + } catch ( RuntimeException $e ) { + return new \WP_Error( + 'prompt_builder_runtime_error', + $e->getMessage(), // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped + array( 'exception' => $e ) + ); + } + } + + /** + * Converts text to speech. + * + * @since n.e.x.t + * + * @return File|\WP_Error The generated speech audio file, or WP_Error on failure. + */ + public function convert_text_to_speech() { + try { + return parent::convertTextToSpeech(); + } catch ( InvalidArgumentException $e ) { + return new \WP_Error( + 'prompt_builder_invalid_argument', + $e->getMessage(), // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped + array( 'exception' => $e ) + ); + } catch ( RuntimeException $e ) { + return new \WP_Error( + 'prompt_builder_runtime_error', + $e->getMessage(), // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped + array( 'exception' => $e ) + ); + } + } + + /** + * Converts text to multiple speech outputs. + * + * @since n.e.x.t + * + * @param int|null $candidate_count The number of speech outputs to generate. + * + * @return list|\WP_Error The generated speech audio files, or WP_Error on failure. + */ + public function convert_text_to_speeches( ?int $candidate_count = null ) { + try { + return parent::convertTextToSpeeches( $candidate_count ); + } catch ( InvalidArgumentException $e ) { + return new \WP_Error( + 'prompt_builder_invalid_argument', + $e->getMessage(), // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped + array( 'exception' => $e ) + ); + } catch ( RuntimeException $e ) { + return new \WP_Error( + 'prompt_builder_runtime_error', + $e->getMessage(), // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped + array( 'exception' => $e ) + ); + } + } + + /** + * Generates speech from the prompt. + * + * @since n.e.x.t + * + * @return File|\WP_Error The generated speech audio file, or WP_Error on failure. + */ + public function generate_speech() { + try { + return parent::generateSpeech(); + } catch ( InvalidArgumentException $e ) { + return new \WP_Error( + 'prompt_builder_invalid_argument', + $e->getMessage(), // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped + array( 'exception' => $e ) + ); + } catch ( RuntimeException $e ) { + return new \WP_Error( + 'prompt_builder_runtime_error', + $e->getMessage(), // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped + array( 'exception' => $e ) + ); + } + } + + /** + * Generates multiple speech outputs from the prompt. + * + * @since n.e.x.t + * + * @param int|null $candidate_count The number of speech outputs to generate. + * + * @return list|\WP_Error The generated speech audio files, or WP_Error on failure. + */ + public function generate_speeches( ?int $candidate_count = null ) { + try { + return parent::generateSpeeches( $candidate_count ); + } catch ( InvalidArgumentException $e ) { + return new \WP_Error( + 'prompt_builder_invalid_argument', + $e->getMessage(), // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped + array( 'exception' => $e ) + ); + } catch ( RuntimeException $e ) { + return new \WP_Error( + 'prompt_builder_runtime_error', + $e->getMessage(), // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped + array( 'exception' => $e ) + ); + } + } +} diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 5dc1d9d..a393316 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,3 +1,6 @@ +includes: + - vendor/szepeviktor/phpstan-wordpress/extension.neon + parameters: level: max paths: