Skip to content
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
5c3b13a
feature/initial-cache-implementation - initial implementation with ba…
chr15k Jan 9, 2025
76af19d
feature/initial-cache-implementation - update gitignore
chr15k Jan 9, 2025
394ed7e
feature/initial-cache-implementation - composer.json updated
chr15k Jan 9, 2025
69bfc8a
feature/initial-cache-implementation - streamline cache directory res…
chr15k Jan 9, 2025
a2fb471
feature/initial-cache-implementation - decouple adapter args from abs…
chr15k Jan 9, 2025
d64bed3
feature/initial-cache-implementation - get phpstan passing
chr15k Jan 9, 2025
2e3b173
feature/initial-cache-implementation - initial aspell cache implement…
chr15k Jan 10, 2025
8554e45
feature/initial-cache-implementation - refactor cache methods and ins…
chr15k Jan 10, 2025
dfcadff
Merge branch 'master' into feature/initial-cache-implementation
chr15k Jan 10, 2025
eb2d58d
feature/initial-cache-implementation - phpcs fixer
chr15k Jan 10, 2025
3942885
Merge branch 'feature/initial-cache-implementation' of github.com:chr…
chr15k Jan 10, 2025
9e6f7b3
feature/initial-cache-implementation - fix phpcs deprecation warning
chr15k Jan 10, 2025
7e4e17d
feature/initial-cache-implementation - fix phpcs deprecation warning
chr15k Jan 10, 2025
0b87946
feature/initial-cache-implementation - revert phpcs native_function_i…
chr15k Jan 10, 2025
fc88d4c
feature/initial-cache-implementation - re-ran phpcs fixer
chr15k Jan 10, 2025
f1b8ffa
feature/initial-cache-implementation - replace symfony/cache with psr…
chr15k Jan 11, 2025
a7bc62a
feature/initial-cache-implementation - rebase master
chr15k Jan 11, 2025
3ea7c67
feature/initial-cache-implementation - update Safe method namespace
chr15k Jan 11, 2025
4d31033
feature/initial-cache-implementation - minor changes
chr15k Jan 12, 2025
c596eec
feature/initial-cache-implementation - minor changes
chr15k Jan 12, 2025
5665dd3
feature/initial-cache-implementation - refactor to psr/cache implemen…
chr15k Jan 14, 2025
9a64c36
feature/initial-cache-implementation - refactor to psr/cache implemen…
chr15k Jan 14, 2025
51ab8f3
feature/initial-cache-implementation - phpstan fixes
chr15k Jan 15, 2025
bd2f4c4
feature/initial-cache-implementation - phpstan fixes
chr15k Jan 15, 2025
f21ccb5
feature/initial-cache-implementation - remove readonly class for php …
chr15k Jan 15, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ build
composer.lock
vendor
.idea
.vscode
.php-cs-fixer.cache
.phpunit.result.cache
cache
.phpspellcheck.cache
composer/
.DS_Store
.aider*
2 changes: 1 addition & 1 deletion .php-cs-fixer.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
'method_chaining_indentation' => true,
'native_function_casing' => true,
'native_function_invocation' => ['include' => ['@compiler_optimized']],
'new_with_braces' => true,
'new_with_parentheses' => true,
'modernize_types_casting' => true,
'method_argument_space' => ['on_multiline' => 'ensure_fully_multiline'],
'no_empty_statement' => true,
Expand Down
5 changes: 4 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"nyholm/psr7": "^1.3",
"psr/http-client": "^1.0",
"symfony/process": "^4.4.30 | ^5.0 |^6.0 | ^7.0",
"symfony/cache": "^4.4.30 | ^5.0 |^6.0 | ^7.0",
"thecodingmachine/safe": "^1.0 | ^2.0",
"webmozart/assert": "^1.11"
},
Expand All @@ -51,7 +52,9 @@
"psr-4": {
"PhpSpellcheck\\": "src"
},
"files": [ "src/Text/functions.php" ]
"files": [
"src/Text/functions.php"
]
},
"autoload-dev": {
"psr-4": {
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ services:
PHP_VERSION: ${PHP_VERSION:-8.1}
volumes:
- .:/usr/src/myapp
- ./cache:/root/composer/cache
- ./composer/cache:/root/composer/cache
environment:
- LANG=en_US.UTF-8
- COMPOSER_CACHE_DIR=/root/composer/cache
Expand Down
47 changes: 47 additions & 0 deletions src/Cache/Cache.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

declare(strict_types=1);

namespace PhpSpellcheck\Cache;

use PhpSpellcheck\Cache\Stores\StoreInterface;
use PhpSpellcheck\Exception\InvalidArgumentException;

class Cache implements CacheFactoryInterface
{
/**
* Get a cache store instance by driver.
*
* @param array<string, mixed> $config
*/
public static function create(?string $driver = null, array $config = []): StoreInterface
{
$driver ??= self::getDefaultDriver();

$class = self::resolveStoreClass($driver);

return $class::create(...$config);
}

/**
* Resolve the cache store class.
*/
public static function resolveStoreClass(string $driver): string
{
$class = \sprintf('%s\%s\%s%s', __NAMESPACE__, 'Stores', ucfirst($driver), 'Store');

if (!class_exists($class)) {
throw new InvalidArgumentException("Cache store [{$driver}] is not defined.");
}

return $class;
}

/**
* Get the default cache driver name.
*/
private static function getDefaultDriver(): string
{
return 'file';
}
}
17 changes: 17 additions & 0 deletions src/Cache/CacheFactoryInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace PhpSpellcheck\Cache;

use PhpSpellcheck\Cache\Stores\StoreInterface;

interface CacheFactoryInterface
{
/**
* Get a cache store instance by name.
*
* @param array<string, mixed> $config
*/
public static function create(?string $name = null, array $config = []): StoreInterface;
}
85 changes: 85 additions & 0 deletions src/Cache/Stores/FileStore.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php

declare(strict_types=1);

namespace PhpSpellcheck\Cache\Stores;

use Composer\Autoload\ClassLoader;
use Symfony\Contracts\Cache\ItemInterface;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;

class FileStore implements StoreInterface
{
/**
* The filesystem adapter instance.
*/
public function __construct(private FilesystemAdapter $filesystemAdapter)
{
//
}

/**
* Create a new file cache store instance.
*/
public static function create(string $namespace = '', int $defaultLifetime = 3600, ?string $cacheDirectory = null): self
{
return new self(new FilesystemAdapter($namespace, $defaultLifetime, $cacheDirectory ?? self::getDefaultCachePath()));
}

/**
* Get the default cache directory for the file store.
*/
public static function getDefaultCachePath(): string
{
return \dirname(array_keys(ClassLoader::getRegisteredLoaders())[0]).'/.phpspellcheck.cache';
}

/**
* Fetches an item from the cache.
*
* @param array<mixed> $metadata
*/
public function get(string $key, callable $callback, float $beta = null, ?array &$metadata = null): mixed
{
return $this->filesystemAdapter->get($key, $callback, $beta, $metadata);
}

/**
* Set the value of the given key in the cache.
*/
public function set(string $key, mixed $value, ?int $lifetime = null): void
{
$item = $this->filesystemAdapter->getItem($key);
$item->set($value);

if ($lifetime !== null) {
$item->expiresAfter($lifetime);
}

$this->filesystemAdapter->save($item);
}

/**
* Removed an item from the pool.
*/
public function delete(string $key): bool
{
return $this->filesystemAdapter->delete($key);
}

/**
* Fetches an item from the cache.
*/
public function getItem(string $key): ItemInterface
{
return $this->filesystemAdapter->getItem($key);
}

/**
* Clear data from cache.
*/
public function clear(string $prefix = ''): bool
{
return $this->filesystemAdapter->clear($prefix);
}
}
26 changes: 26 additions & 0 deletions src/Cache/Stores/StoreInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace PhpSpellcheck\Cache\Stores;

use Symfony\Contracts\Cache\ItemInterface;
use Symfony\Contracts\Cache\CacheInterface;

interface StoreInterface extends CacheInterface
{
/**
* Clear data from cache.
*/
public function clear(string $prefix = ''): bool;

/**
* Fetches an item from the cache.
*/
public function getItem(string $key): ItemInterface;

/**
* Create a new cache store instance.
*/
public static function create(string $namespace = '', int $defaultLifetime = 3600, ?string $cacheDirectory = null): self;
}
2 changes: 1 addition & 1 deletion src/MisspellingHandler/EchoHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class EchoHandler implements MisspellingHandlerInterface
public function handle(iterable $misspellings): void
{
foreach ($misspellings as $misspelling) {
$output = sprintf(
$output = \sprintf(
'word: %s | line: %d | offset: %d | suggestions: %s | context: %s' . PHP_EOL,
$misspelling->getWord(),
$misspelling->getLineNumber(),
Expand Down
67 changes: 42 additions & 25 deletions src/Spellchecker/Aspell.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace PhpSpellcheck\Spellchecker;

use PhpSpellcheck\Exception\ProcessHasErrorOutputException;
use PhpSpellcheck\Traits\SpellcheckerCacheTrait;
use PhpSpellcheck\Utils\CommandLine;
use PhpSpellcheck\Utils\IspellParser;
use PhpSpellcheck\Utils\ProcessRunner;
Expand All @@ -13,37 +14,19 @@

class Aspell implements SpellcheckerInterface
{
/**
* @var CommandLine
*/
private $binaryPath;
use SpellcheckerCacheTrait;

public function __construct(CommandLine $binaryPath)
public function __construct(private CommandLine $binaryPath)
{
$this->binaryPath = $binaryPath;
$this->initCache();
}

/**
* {@inheritdoc}
*/
public function check(string $text, array $languages = [], array $context = []): iterable
{
Assert::maxCount($languages, 1, 'Aspell spellchecker doesn\'t support multiple languages check');

$cmd = $this->binaryPath->addArgs(['--encoding', 'utf-8']);
$cmd = $cmd->addArg('-a');

if (!empty($languages)) {
$cmd = $cmd->addArg('--lang=' . implode(',', $languages));
}

$process = new Process($cmd->getArgs());
// Add prefix characters putting Ispell's type of spellcheckers in terse-mode,
// ignoring correct words and thus speeding up the execution
$process->setInput('!' . PHP_EOL . IspellParser::adaptInputForTerseModeProcessing($text) . PHP_EOL . '%');

$output = ProcessRunner::run($process)->getOutput();

if ($process->getErrorOutput() !== '') {
throw new ProcessHasErrorOutputException($process->getErrorOutput(), $text, $process->getCommandLine());
}
$output = $this->processText($text, $languages);

return IspellParser::parseMisspellingsFromOutput($output, $context);
}
Expand Down Expand Up @@ -78,4 +61,38 @@ public static function create(?string $binaryPathAsString = null): self
{
return new self(new CommandLine($binaryPathAsString ?? 'aspell'));
}

/**
* Process the text with Aspell spellchecker.
*
* @param array<int, string> $languages
*/
protected function processText(string $text, array $languages): string
{
Assert::maxCount($languages, 1, 'Aspell spellchecker doesn\'t support multiple languages check');

$cmd = $this->binaryPath->addArgs(['--encoding', 'utf-8']);
$cmd = $cmd->addArg('-a');

if (!empty($languages)) {
$cmd = $cmd->addArg('--lang='.implode(',', $languages));
}

$process = new Process($cmd->getArgs());
// Add prefix characters putting Ispell's type of spellcheckers in terse-mode,
// ignoring correct words and thus speeding up the execution
$process->setInput('!'.PHP_EOL.IspellParser::adaptInputForTerseModeProcessing($text).PHP_EOL.'%');

$cacheKey = $this->getCacheKey($text, $languages);

return $this->cache->get($cacheKey, function () use (&$process, $text) {
$process = ProcessRunner::run($process);

if ($process->getErrorOutput() !== '') {
throw new ProcessHasErrorOutputException($process->getErrorOutput(), $text, $process->getCommandLine());
}

return $process->getOutput();
});
}
}
48 changes: 48 additions & 0 deletions src/Traits/SpellcheckerCacheTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

declare(strict_types=1);

namespace PhpSpellcheck\Traits;

use PhpSpellcheck\Cache\Cache;
use PhpSpellcheck\Cache\Stores\StoreInterface;

trait SpellcheckerCacheTrait
{
/**
* The cache store instance.
*/
private StoreInterface $cache;

/**
* The cache store instance.
*
* @param array<string, mixed> $config
*/
private function initCache(array $config = []): void
{
$config['namespace'] ??= $this->getCacheNamespace();

$this->cache = Cache::create(config: $config);
}

/**
* Get the cache key for the given text and languages.
*
* @param array<int, string> $languages
*/
private function getCacheKey(string $text, array $languages): string
{
return md5(\sprintf('%s_%s', $text, implode('_', $languages)));
}

/**
* Get the cache namespace.
*/
private function getCacheNamespace(): string
{
$parts = explode('\\', \get_class($this));

return end($parts);
}
}
Loading
Loading