Add PSR HTTP Message Interfaces and Dependencies

- Implemented StreamInterface, UploadedFileInterface, and UriInterface as per PSR standards.
- Added getallheaders function to retrieve HTTP headers in a compatible manner.
- Included LICENSE files for ralouphie/getallheaders and symfony/deprecation-contracts.
- Introduced function for triggering deprecation notices in Symfony.
This commit is contained in:
2025-12-28 12:44:00 +01:00
parent cf600ae727
commit cd264483f8
410 changed files with 60841 additions and 16 deletions

View File

@@ -0,0 +1,163 @@
<?php
/**
* Class ATFPP\AI_Translate\Services\Util\AI_Capabilities
*
* @since 0.1.0
* @package ai-services
*/
namespace ATFPP\AI_Translate\Services\Util;
use ATFPP\AI_Translate\Services\API\Enums\AI_Capability;
use ATFPP\AI_Translate\Services\API\Types\Model_Metadata;
use ATFPP\AI_Translate\Services\Contracts\Generative_AI_Model;
use ATFPP\AI_Translate\Services\Contracts\With_Function_Calling;
use ATFPP\AI_Translate\Services\Contracts\With_Multimodal_Input;
use ATFPP\AI_Translate\Services\Contracts\With_Multimodal_Output;
use ATFPP\AI_Translate\Services\Contracts\With_Text_Generation;
use ATFPP\AI_Translate\Services\Contracts\With_Web_Search;
use InvalidArgumentException;
/**
* Class exposing the available AI capabilities and related static utility methods.
*
* @since 0.1.0
*/
final class AI_Capabilities {
/**
* Gets the combined AI capabilities that the given model classes support.
*
* @since 0.7.0
*
* @param string[] $model_classes The model class names.
* @return string[] The AI capabilities that the model classes support, based on the interfaces they implement.
*/
public static function get_model_classes_capabilities( array $model_classes ): array {
$capabilities = array();
foreach ( $model_classes as $model_class ) {
$model_capabilities = self::get_model_class_capabilities( $model_class );
foreach ( $model_capabilities as $capability ) {
$capabilities[] = $capability;
}
}
return array_unique( $capabilities );
}
/**
* Gets the AI capabilities that the given model class supports.
*
* @since 0.1.0
*
* @param string $model_class The model class name.
* @return string[] The AI capabilities that the model class supports, based on the interfaces it implements.
*
* @SuppressWarnings(PHPMD.NPathComplexity)
*/
public static function get_model_class_capabilities( string $model_class ): array {
$interfaces = class_implements( $model_class );
$capabilities = array();
if ( isset( $interfaces[ With_Function_Calling::class ] ) ) {
$capabilities[] = AI_Capability::FUNCTION_CALLING;
}
if ( isset( $interfaces[ With_Multimodal_Input::class ] ) ) {
$capabilities[] = AI_Capability::MULTIMODAL_INPUT;
}
if ( isset( $interfaces[ With_Multimodal_Output::class ] ) ) {
$capabilities[] = AI_Capability::MULTIMODAL_OUTPUT;
}
if ( isset( $interfaces[ With_Text_Generation::class ] ) ) {
$capabilities[] = AI_Capability::TEXT_GENERATION;
}
if ( isset( $interfaces[ With_Web_Search::class ] ) ) {
$capabilities[] = AI_Capability::WEB_SEARCH;
}
return $capabilities;
}
/**
* Gets the AI capabilities that the given model instance supports.
*
* @since 0.5.0
*
* @param Generative_AI_Model $model The model instance.
* @return string[] The AI capabilities that the model instance supports, based on the interfaces it implements.
*
* @SuppressWarnings(PHPMD.NPathComplexity)
*/
public static function get_model_instance_capabilities( Generative_AI_Model $model ): array {
$capabilities = array();
if ( $model instanceof With_Function_Calling ) {
$capabilities[] = AI_Capability::FUNCTION_CALLING;
}
if ( $model instanceof With_Multimodal_Input ) {
$capabilities[] = AI_Capability::MULTIMODAL_INPUT;
}
if ( $model instanceof With_Multimodal_Output ) {
$capabilities[] = AI_Capability::MULTIMODAL_OUTPUT;
}
if ( $model instanceof With_Text_Generation ) {
$capabilities[] = AI_Capability::TEXT_GENERATION;
}
if ( $model instanceof With_Web_Search ) {
$capabilities[] = AI_Capability::WEB_SEARCH;
}
return $capabilities;
}
/**
* Gets the model slugs that satisfy the given capabilities.
*
* @since 0.1.0
* @since 0.5.0 Now expects an array of model data shapes, mapped by model slug.
* @since 0.7.0 Now expects a map of model metadata objects.
*
* @param array<string, Model_Metadata> $models Metadata for each model, mapped by model slug.
* @param string[] $capabilities The required capabilities that the models should satisfy.
* @return string[] Slugs of all models that satisfy the given capabilities.
*
* @throws InvalidArgumentException Thrown if no model satisfies the given capabilities.
*/
public static function get_model_slugs_for_capabilities( array $models, array $capabilities ): array {
$model_slugs = array();
foreach ( $models as $model_slug => $model_metadata ) {
$model_capabilities = $model_metadata->get_capabilities();
if ( ! array_diff( $capabilities, $model_capabilities ) ) {
$model_slugs[] = $model_slug;
}
}
if ( ! $model_slugs ) {
throw new InvalidArgumentException(
'No model satisfies the given capabilities.'
);
}
return $model_slugs;
}
/**
* Gets the model class name from the given model class names that satisfies the given capabilities.
*
* @since 0.7.0
*
* @param string[] $model_classes The model class names.
* @param string[] $capabilities The required capabilities that the models should satisfy.
* @return string The model class name that satisfies the given capabilities.
*
* @throws InvalidArgumentException Thrown if no model satisfies the given capabilities.
*/
public static function get_model_class_for_capabilities( array $model_classes, array $capabilities ): string {
foreach ( $model_classes as $model_class ) {
$model_capabilities = self::get_model_class_capabilities( $model_class );
if ( ! array_diff( $capabilities, $model_capabilities ) ) {
return $model_class;
}
}
throw new InvalidArgumentException(
'No model class satisfies the given capabilities.'
);
}
}

View File

@@ -0,0 +1,153 @@
<?php
/**
* Class ATFPP\AI_Translate\Services\Util\Data_Encryption
*
* @since 0.1.0
* @package ai-services
*/
namespace ATFPP\AI_Translate\Services\Util;
/**
* Class responsible for encrypting and decrypting data.
*
* @since 0.1.0
* @see https://felix-arntz.me/blog/storing-confidential-data-in-wordpress/
*/
final class Data_Encryption {
/**
* Key to use for encryption.
*
* @since 0.1.0
* @var string
*/
private $key;
/**
* Salt to use for encryption.
*
* @since 0.1.0
* @var string
*/
private $salt;
/**
* Constructor.
*
* @since 0.1.0
*
* @param ?string $key Optional. Key to use for encryption. If not passed, the default key determined by constants
* will be used.
* @param ?string $salt Optional. Salt to use for encryption. If not passed, the default salt determined by
* constants will be used.
*/
public function __construct( ?string $key = null, ?string $salt = null ) {
$this->key = $key ?? $this->get_default_key();
$this->salt = $salt ?? $this->get_default_salt();
}
/**
* Encrypts a value.
*
* If a user-based key is set, that key is used. Otherwise the default key is used.
*
* @since 0.1.0
*
* @param string $value Value to encrypt.
* @return string Encrypted value, or empty string on failure.
*/
public function encrypt( string $value ): string {
if ( ! extension_loaded( 'openssl' ) ) {
return $value;
}
$method = 'aes-256-ctr';
$ivlen = openssl_cipher_iv_length( $method );
$iv = openssl_random_pseudo_bytes( $ivlen );
$raw_value = openssl_encrypt( $value . $this->salt, $method, $this->key, 0, $iv );
if ( ! $raw_value ) {
return '';
}
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
return base64_encode( $iv . $raw_value );
}
/**
* Decrypts a value.
*
* If a user-based key is set, that key is used. Otherwise the default key is used.
*
* @since 0.1.0
*
* @param string $raw_value Value to decrypt.
* @return string Decrypted value, or empty string on failure.
*/
public function decrypt( string $raw_value ): string {
if ( ! extension_loaded( 'openssl' ) ) {
return $raw_value;
}
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
$decoded_value = base64_decode( $raw_value, true );
if ( false === $decoded_value ) {
return '';
}
$method = 'aes-256-ctr';
$ivlen = openssl_cipher_iv_length( $method );
$iv = substr( $decoded_value, 0, $ivlen );
$decoded_value = substr( $decoded_value, $ivlen );
$value = openssl_decrypt( $decoded_value, $method, $this->key, 0, $iv );
if ( ! $value || substr( $value, - strlen( $this->salt ) ) !== $this->salt ) {
return '';
}
return substr( $value, 0, - strlen( $this->salt ) );
}
/**
* Gets the default encryption key to use.
*
* @since 0.1.0
*
* @return string Default (not user-based) encryption key.
*/
private function get_default_key(): string {
if ( defined( 'AI_SERVICES_ENCRYPTION_KEY' ) && '' !== AI_SERVICES_ENCRYPTION_KEY ) {
return AI_SERVICES_ENCRYPTION_KEY;
}
if ( defined( 'LOGGED_IN_KEY' ) && '' !== LOGGED_IN_KEY ) {
return LOGGED_IN_KEY;
}
// If this is reached, you're either not on a live site or have a serious security issue.
return 'test-key';
}
/**
* Gets the default encryption salt to use.
*
* @since 0.1.0
*
* @return string Encryption salt.
*/
private function get_default_salt(): string {
if ( defined( 'AI_SERVICES_ENCRYPTION_SALT' ) && '' !== AI_SERVICES_ENCRYPTION_SALT ) {
return AI_SERVICES_ENCRYPTION_SALT;
}
if ( defined( 'LOGGED_IN_SALT' ) && '' !== LOGGED_IN_SALT ) {
return LOGGED_IN_SALT;
}
// If this is reached, you're either not on a live site or have a serious security issue.
return 'test-salt';
}
}

View File

@@ -0,0 +1,136 @@
<?php
/**
* Class ATFPP\AI_Translate\Services\Util\Formatter
*
* @since 0.1.0
* @package ai-services
*/
namespace ATFPP\AI_Translate\Services\Util;
use ATFPP\AI_Translate\Services\API\Enums\AI_Capability;
use ATFPP\AI_Translate\Services\API\Enums\Content_Role;
use ATFPP\AI_Translate\Services\API\Types\Content;
use ATFPP\AI_Translate\Services\API\Types\Parts;
use ATFPP\AI_Translate\Services\API\Types\Parts\Text_Part;
use InvalidArgumentException;
/**
* Class providing static methods for formatting content.
*
* @since 0.1.0
*/
final class Formatter {
/**
* Formats and validates the various supported formats of a user prompt into a consistent list of Content instances.
*
* This method takes into account whether the provided content is supported by the given model, based on its capabilities.
*
* @since 0.5.0
*
* @param string|Parts|Content|Content[] $content The content to format.
* @param string[] $capabilities The AI capabilities that the model supports.
* @return Content[] The formatted Content instances.
*
* @throws InvalidArgumentException Thrown if the content is invalid or the model does not support it.
*/
public static function format_and_validate_new_contents( $content, array $capabilities ): array {
if ( is_array( $content ) ) {
$contents = array_map(
array( __CLASS__, 'format_new_content' ),
$content
);
} else {
$contents = array( self::format_new_content( $content ) );
}
if ( count( $contents ) === 0 ) {
throw new InvalidArgumentException(
'No prompt was provided.'
);
}
if ( Content_Role::USER !== $contents[0]->get_role() ) {
throw new InvalidArgumentException(
'The first Content instance in the conversation or prompt must be user content.'
);
}
if ( ! in_array( AI_Capability::CHAT_HISTORY, $capabilities, true ) && count( $contents ) > 1 ) {
throw new InvalidArgumentException(
'The model does not support chat history. Only one content prompt must be provided.'
);
}
if ( ! in_array( AI_Capability::MULTIMODAL_INPUT, $capabilities, true ) ) {
// For performance reasons, only check the last content prompt, which likely is the only new one.
$last_content = $contents[ count( $contents ) - 1 ];
$last_parts = $last_content->get_parts();
$last_parts_text_only = $last_parts->filter( array( 'class_name' => Text_Part::class ) );
if ( count( $last_parts_text_only ) < count( $last_parts ) ) {
throw new InvalidArgumentException(
'The model does not support multimodal input. Only text parts must be provided.'
);
}
}
return $contents;
}
/**
* Formats the various supported formats of new user content into a consistent Content instance.
*
* @since 0.1.0
*
* @param string|Parts|Content $content The content to format.
* @return Content The formatted new content.
*/
public static function format_new_content( $content ): Content {
return self::format_content( $content, Content_Role::USER );
}
/**
* Formats the various supported formats of a system instruction into a consistent Content instance.
*
* @since 0.1.0
*
* @param string|Parts|Content $input The system instruction to format.
* @return Content The formatted system instruction.
*/
public static function format_system_instruction( $input ): Content {
return self::format_content( $input, Content_Role::SYSTEM );
}
/**
* Formats the various supported formats of content into a consistent Content instance.
*
* @since 0.1.0
*
* @param string|Parts|Content $input The content to format.
* @param string $role The role for the content.
* @return Content The formatted content.
*
* @throws InvalidArgumentException Thrown if the value is not a string, a Parts instance, or a Content instance.
*/
public static function format_content( $input, string $role ): Content {
if ( is_string( $input ) ) {
$parts = new Parts();
$parts->add_text_part( $input );
return new Content( $role, $parts );
}
if ( $input instanceof Parts ) {
return new Content( $role, $input );
}
if ( ! $input instanceof Content ) {
throw new InvalidArgumentException(
'The value must be a string, a Parts instance, or a Content instance.'
);
}
return $input;
}
}

View File

@@ -0,0 +1,29 @@
<?php
/**
* Class ATFPP\AI_Translate\Services\Util\Strings
*
* @since 0.2.0
* @package ai-services
*/
namespace ATFPP\AI_Translate\Services\Util;
/**
* Class providing static methods for string operations.
*
* @since 0.2.0
*/
final class Strings {
/**
* Converts a snake_case string to a camelCase string.
*
* @since 0.2.0
*
* @param string $input The snake_case string.
* @return string The camelCase string.
*/
public static function snake_case_to_camel_case( string $input ): string {
return lcfirst( str_replace( '_', '', ucwords( $input, '_' ) ) );
}
}

View File

@@ -0,0 +1,99 @@
<?php
/**
* Class ATFPP\AI_Translate\Services\Util\Transformer
*
* @since 0.2.0
* @package ai-services
*/
namespace ATFPP\AI_Translate\Services\Util;
use ATFPP\AI_Translate\Services\API\Types\Content;
use ATFPP\AI_Translate\Services\Contracts\Generation_Config;
use InvalidArgumentException;
/**
* Class providing static methods for transforming data.
*
* @since 0.2.0
*/
final class Transformer {
/**
* Transforms the given content using the provided transformers.
*
* @since 0.2.0
*
* @param Content $content The content to transform.
* @param array<string, callable> $transformers The transformers to use. Each transformer callback should accept
* the content as its only parameter and return the transformed value
* for its key.
* @return array<string, mixed> The transformed content.
*
* @throws InvalidArgumentException Thrown if a provided transformer is not callable.
*/
public static function transform_content( Content $content, array $transformers ): array {
$data = array();
foreach ( $transformers as $key => $transformer ) {
if ( ! is_callable( $transformer ) ) {
throw new InvalidArgumentException(
sprintf(
'The transformer for key %s is invalid.',
htmlspecialchars( $key ) // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
)
);
}
// Transform the value and set it if truthy.
$value = $transformer( $content );
if ( ! $value ) {
continue;
}
$data[ $key ] = $value;
}
return $data;
}
/**
* Merges the given Generation_Config instance into the given parameters using the provided transformers.
*
* @since 0.2.0
*
* @param array<string, mixed> $params The parameters to merge the generation config into.
* @param Generation_Config $config The generation config to use for the transformation.
* @param array<string, callable> $transformers The transformers to use. Each transformer callback should accept
* the generation config as its only parameter and return the
* transformed value for its key.
* @return array<string, mixed> The transformed parameters.
*
* @throws InvalidArgumentException Thrown if a provided transformer is not callable.
*/
public static function transform_generation_config_params( array $params, Generation_Config $config, array $transformers ): array {
foreach ( $transformers as $key => $transformer ) {
if ( ! is_callable( $transformer ) ) {
throw new InvalidArgumentException(
sprintf(
'The transformer for key %s is invalid.',
htmlspecialchars( $key ) // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
)
);
}
// Already set parameters take precedence.
if ( isset( $params[ $key ] ) ) {
continue;
}
// Transform the value and set it if truthy.
$value = $transformer( $config );
if ( ! $value ) {
continue;
}
$params[ $key ] = $value;
}
return $params;
}
}