fix(Docker): change ENV arg from 'prod' to 'production' to match actual ini filename
This commit is contained in:
@@ -51,7 +51,7 @@ RUN curl -sS https://getcomposer.org/installer | php \
|
|||||||
&& mv composer.phar /usr/local/bin/composer
|
&& mv composer.phar /usr/local/bin/composer
|
||||||
|
|
||||||
# Installiere Xdebug nur im Entwicklungsmodus
|
# Installiere Xdebug nur im Entwicklungsmodus
|
||||||
ARG ENV=prod
|
ARG ENV=production
|
||||||
RUN if [ "$ENV" = "dev" ]; then \
|
RUN if [ "$ENV" = "dev" ]; then \
|
||||||
pecl install xdebug \
|
pecl install xdebug \
|
||||||
&& docker-php-ext-enable xdebug; \
|
&& docker-php-ext-enable xdebug; \
|
||||||
@@ -108,3 +108,136 @@ RUN apt-get update && apt-get install -y gosu && apt-get clean && rm -rf /var/li
|
|||||||
|
|
||||||
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
|
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
|
||||||
CMD ["php-fpm"]
|
CMD ["php-fpm"]
|
||||||
|
|
||||||
|
#############################################
|
||||||
|
# Production Stages (Extended Build)
|
||||||
|
#############################################
|
||||||
|
|
||||||
|
# Stage: Composer Dependencies (Production)
|
||||||
|
FROM base AS composer-deps-production
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy composer files
|
||||||
|
COPY composer.json composer.lock ./
|
||||||
|
|
||||||
|
# Install production dependencies only
|
||||||
|
RUN composer install \
|
||||||
|
--no-dev \
|
||||||
|
--no-scripts \
|
||||||
|
--no-autoloader \
|
||||||
|
--ignore-platform-reqs \
|
||||||
|
--prefer-dist
|
||||||
|
|
||||||
|
# Copy application code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Generate optimized autoloader
|
||||||
|
RUN composer dump-autoload --optimize --classmap-authoritative
|
||||||
|
|
||||||
|
# Stage: Frontend Build
|
||||||
|
FROM node:20-alpine AS frontend-build
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package files
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
|
||||||
|
# Install npm dependencies
|
||||||
|
RUN npm ci --production=false
|
||||||
|
|
||||||
|
# Copy source files needed for build
|
||||||
|
COPY resources ./resources
|
||||||
|
COPY vite.config.js ./
|
||||||
|
COPY tsconfig.json ./
|
||||||
|
|
||||||
|
# Build production assets
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Stage: Production Runtime
|
||||||
|
FROM php:8.5.0RC3-fpm AS production
|
||||||
|
|
||||||
|
# Install system dependencies + nginx for production
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
nginx \
|
||||||
|
curl \
|
||||||
|
libzip-dev \
|
||||||
|
libpng-dev \
|
||||||
|
libjpeg-dev \
|
||||||
|
libfreetype6-dev \
|
||||||
|
libwebp-dev \
|
||||||
|
libavif-dev \
|
||||||
|
libxpm-dev \
|
||||||
|
libsodium-dev \
|
||||||
|
libpq-dev \
|
||||||
|
&& apt-get clean \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Install PHP extensions (same as base)
|
||||||
|
RUN docker-php-ext-configure gd \
|
||||||
|
--with-freetype \
|
||||||
|
--with-jpeg \
|
||||||
|
--with-webp \
|
||||||
|
--with-avif \
|
||||||
|
--with-xpm \
|
||||||
|
&& docker-php-ext-install -j$(nproc) gd
|
||||||
|
|
||||||
|
RUN docker-php-ext-install -j$(nproc) \
|
||||||
|
zip \
|
||||||
|
pdo \
|
||||||
|
pdo_mysql \
|
||||||
|
pdo_pgsql \
|
||||||
|
pcntl \
|
||||||
|
posix \
|
||||||
|
shmop \
|
||||||
|
bcmath \
|
||||||
|
sodium
|
||||||
|
|
||||||
|
# Install PECL extensions
|
||||||
|
RUN pecl install apcu redis-6.3.0RC1 \
|
||||||
|
&& docker-php-ext-enable apcu redis
|
||||||
|
|
||||||
|
# Configure APCu
|
||||||
|
RUN echo "apc.enable_cli=1" >> /usr/local/etc/php/conf.d/docker-php-ext-apcu.ini \
|
||||||
|
&& echo "apc.shm_size=128M" >> /usr/local/etc/php/conf.d/docker-php-ext-apcu.ini
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /var/www/html
|
||||||
|
|
||||||
|
# Configure PHP for production
|
||||||
|
COPY docker/php/php.production.ini /usr/local/etc/php/conf.d/zzz-custom.ini
|
||||||
|
COPY docker/php/opcache.ini /usr/local/etc/php/conf.d/opcache.ini
|
||||||
|
|
||||||
|
# Configure PHP-FPM (clear_env = no for Docker secrets)
|
||||||
|
COPY docker/php/www.production.conf /usr/local/etc/php-fpm.d/www.conf
|
||||||
|
|
||||||
|
# Configure Nginx for Traefik integration
|
||||||
|
COPY docker/nginx/nginx.production.conf /etc/nginx/nginx.conf
|
||||||
|
COPY docker/nginx/default.traefik.conf /etc/nginx/sites-available/default
|
||||||
|
RUN ln -sf /etc/nginx/sites-available/default /etc/nginx/sites-enabled/default
|
||||||
|
|
||||||
|
# Copy application code and dependencies
|
||||||
|
COPY --chown=www-data:www-data . .
|
||||||
|
COPY --from=composer-deps-production --chown=www-data:www-data /app/vendor ./vendor
|
||||||
|
COPY --from=frontend-build --chown=www-data:www-data /app/public/assets ./public/assets
|
||||||
|
COPY --from=frontend-build --chown=www-data:www-data /app/public/.vite ./public/.vite
|
||||||
|
|
||||||
|
# Create required directories
|
||||||
|
RUN mkdir -p storage/logs storage/cache storage/uploads \
|
||||||
|
&& chown -R www-data:www-data storage \
|
||||||
|
&& chmod -R 775 storage
|
||||||
|
|
||||||
|
# Copy entrypoint script for production
|
||||||
|
COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||||
|
RUN chmod +x /usr/local/bin/entrypoint.sh
|
||||||
|
|
||||||
|
# Healthcheck
|
||||||
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \
|
||||||
|
CMD curl -f http://localhost/health || exit 1
|
||||||
|
|
||||||
|
# Expose HTTP only (Traefik handles HTTPS)
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
# Use production entrypoint
|
||||||
|
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
|
||||||
|
CMD ["nginx-php-fpm"]
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
; php.ini für Produktion
|
|
||||||
include = php.common.ini
|
|
||||||
|
|
||||||
[opcache]
|
|
||||||
; Aktiviere OPcache
|
|
||||||
opcache.enable=1
|
|
||||||
; Aktiviere OPcache für CLI-Anwendungen (optional)
|
|
||||||
opcache.enable_cli=0
|
|
||||||
; Maximale Speichernutzung für Cache in MB
|
|
||||||
opcache.memory_consumption=128
|
|
||||||
; Maximale Anzahl an gecachten Dateien
|
|
||||||
opcache.max_accelerated_files=10000
|
|
||||||
; Wie oft wird der Cache validiert (0 = bei jedem Request)
|
|
||||||
; TEMPORÄR: 0 für einfachere Deployments während aktiver Entwicklung
|
|
||||||
; SPÄTER: Auf 60 erhöhen wenn System stabil ist
|
|
||||||
opcache.revalidate_freq=0
|
|
||||||
; Cache-Zeitstempel prüfen
|
|
||||||
; TEMPORÄR: 1 aktiviert für Deployment-Flexibilität
|
|
||||||
; SPÄTER: Auf 0 setzen für maximale Performance wenn stabil
|
|
||||||
opcache.validate_timestamps=1
|
|
||||||
; Performance-Optimierungen
|
|
||||||
opcache.interned_strings_buffer=16
|
|
||||||
; JIT (Just-In-Time Compilation) - Optional für PHP 8.0+
|
|
||||||
opcache.jit_buffer_size=100M
|
|
||||||
opcache.jit=1255
|
|
||||||
|
|
||||||
|
|
||||||
display_errors = Off
|
|
||||||
display_startup_errors = Off
|
|
||||||
error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT
|
|
||||||
memory_limit = 256M
|
|
||||||
upload_max_filesize = 10M
|
|
||||||
post_max_size = 12M
|
|
||||||
max_execution_time = 30
|
|
||||||
@@ -10,11 +10,14 @@ opcache.enable_cli=0
|
|||||||
opcache.memory_consumption=128
|
opcache.memory_consumption=128
|
||||||
; Maximale Anzahl an gecachten Dateien
|
; Maximale Anzahl an gecachten Dateien
|
||||||
opcache.max_accelerated_files=10000
|
opcache.max_accelerated_files=10000
|
||||||
; Wie oft wird der Cache validiert (0 = bei jedem Request, empfohlen für Entwicklung)
|
; Wie oft wird der Cache validiert (0 = bei jedem Request)
|
||||||
; In Produktion höher setzen für bessere Performance
|
; TEMPORÄR: 0 für einfachere Deployments während aktiver Entwicklung
|
||||||
opcache.revalidate_freq=60
|
; SPÄTER: Auf 60 erhöhen wenn System stabil ist
|
||||||
; Cache-Zeitstempel prüfen (0 für Produktionsumgebungen)
|
opcache.revalidate_freq=0
|
||||||
opcache.validate_timestamps=0
|
; Cache-Zeitstempel prüfen
|
||||||
|
; TEMPORÄR: 1 aktiviert für Deployment-Flexibilität
|
||||||
|
; SPÄTER: Auf 0 setzen für maximale Performance wenn stabil
|
||||||
|
opcache.validate_timestamps=1
|
||||||
; Performance-Optimierungen
|
; Performance-Optimierungen
|
||||||
opcache.interned_strings_buffer=16
|
opcache.interned_strings_buffer=16
|
||||||
; JIT (Just-In-Time Compilation) - Optional für PHP 8.0+
|
; JIT (Just-In-Time Compilation) - Optional für PHP 8.0+
|
||||||
|
|||||||
58
src/Framework/Cache/Strategies/AccessPattern.php
Normal file
58
src/Framework/Cache/Strategies/AccessPattern.php
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Framework\Cache\Strategies;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Access pattern tracking for adaptive TTL calculation
|
||||||
|
*/
|
||||||
|
final class AccessPattern
|
||||||
|
{
|
||||||
|
/** @var array<int, Timestamp> */
|
||||||
|
private array $accessTimes = [];
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly int $windowSize
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function recordAccess(): void
|
||||||
|
{
|
||||||
|
$this->accessTimes[] = Timestamp::now();
|
||||||
|
|
||||||
|
// Keep only recent accesses within the window
|
||||||
|
if (count($this->accessTimes) > $this->windowSize) {
|
||||||
|
array_shift($this->accessTimes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRecentAccessCount(): int
|
||||||
|
{
|
||||||
|
return count($this->accessTimes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTotalAccesses(): int
|
||||||
|
{
|
||||||
|
return count($this->accessTimes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate access frequency (accesses per hour)
|
||||||
|
*/
|
||||||
|
public function getAccessFrequency(): float
|
||||||
|
{
|
||||||
|
if (count($this->accessTimes) < 2) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$oldest = $this->accessTimes[0];
|
||||||
|
$newest = end($this->accessTimes);
|
||||||
|
$timeSpan = $newest->diff($oldest);
|
||||||
|
|
||||||
|
if ($timeSpan->toSeconds() <= 0) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$hours = $timeSpan->toSeconds() / 3600.0;
|
||||||
|
|
||||||
|
return count($this->accessTimes) / $hours;
|
||||||
|
}
|
||||||
|
}
|
||||||
44
src/Framework/Cache/Strategies/AdaptiveTtlStats.php
Normal file
44
src/Framework/Cache/Strategies/AdaptiveTtlStats.php
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Framework\Cache\Strategies;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Statistics for adaptive TTL optimization
|
||||||
|
*/
|
||||||
|
final class AdaptiveTtlStats
|
||||||
|
{
|
||||||
|
private int $hits = 0;
|
||||||
|
|
||||||
|
private int $misses = 0;
|
||||||
|
|
||||||
|
public function recordHitMiss(bool $isHit): void
|
||||||
|
{
|
||||||
|
if ($isHit) {
|
||||||
|
$this->hits++;
|
||||||
|
} else {
|
||||||
|
$this->misses++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getHitRate(): float
|
||||||
|
{
|
||||||
|
$total = $this->hits + $this->misses;
|
||||||
|
|
||||||
|
return $total > 0 ? ($this->hits / $total) : 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTotalRequests(): int
|
||||||
|
{
|
||||||
|
return $this->hits + $this->misses;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getHits(): int
|
||||||
|
{
|
||||||
|
return $this->hits;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMisses(): int
|
||||||
|
{
|
||||||
|
return $this->misses;
|
||||||
|
}
|
||||||
|
}
|
||||||
90
src/Framework/Cache/Strategies/HeatMapEntry.php
Normal file
90
src/Framework/Cache/Strategies/HeatMapEntry.php
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Framework\Cache\Strategies;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Heat map entry for tracking cache key usage
|
||||||
|
*/
|
||||||
|
final class HeatMapEntry
|
||||||
|
{
|
||||||
|
/** @var array<array{timestamp: Timestamp, is_hit: bool, retrieval_time: ?float}> */
|
||||||
|
private array $accesses = [];
|
||||||
|
|
||||||
|
private int $totalHits = 0;
|
||||||
|
|
||||||
|
private int $totalMisses = 0;
|
||||||
|
|
||||||
|
private float $totalRetrievalTime = 0.0;
|
||||||
|
|
||||||
|
private int $retrievalTimeCount = 0;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly CacheKey $key
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function recordAccess(bool $isHit, ?Duration $retrievalTime = null): void
|
||||||
|
{
|
||||||
|
$this->accesses[] = [
|
||||||
|
'timestamp' => Timestamp::now(),
|
||||||
|
'is_hit' => $isHit,
|
||||||
|
'retrieval_time' => $retrievalTime?->toSeconds(),
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($isHit) {
|
||||||
|
$this->totalHits++;
|
||||||
|
} else {
|
||||||
|
$this->totalMisses++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($retrievalTime !== null) {
|
||||||
|
$this->totalRetrievalTime += $retrievalTime->toSeconds();
|
||||||
|
$this->retrievalTimeCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep only recent data to prevent memory bloat
|
||||||
|
$cutoff = Timestamp::now()->subtract(Duration::fromHours(48)); // Keep 48 hours
|
||||||
|
$this->accesses = array_filter(
|
||||||
|
$this->accesses,
|
||||||
|
fn($access) => $access['timestamp']->isAfter($cutoff)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRecentAccesses(Timestamp $since): array
|
||||||
|
{
|
||||||
|
return array_filter(
|
||||||
|
$this->accesses,
|
||||||
|
fn($access) => $access['timestamp']->isAfter($since)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getHitRate(): float
|
||||||
|
{
|
||||||
|
$total = $this->totalHits + $this->totalMisses;
|
||||||
|
|
||||||
|
return $total > 0 ? ($this->totalHits / $total) : 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAverageRetrievalTime(): float
|
||||||
|
{
|
||||||
|
return $this->retrievalTimeCount > 0 ? ($this->totalRetrievalTime / $this->retrievalTimeCount) : 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTotalAccesses(): int
|
||||||
|
{
|
||||||
|
return count($this->accesses);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLastAccessTime(): ?Timestamp
|
||||||
|
{
|
||||||
|
if (empty($this->accesses)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return end($this->accesses)['timestamp'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getKey(): CacheKey
|
||||||
|
{
|
||||||
|
return $this->key;
|
||||||
|
}
|
||||||
|
}
|
||||||
71
src/Framework/Cache/Strategies/PredictionPattern.php
Normal file
71
src/Framework/Cache/Strategies/PredictionPattern.php
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Framework\Cache\Strategies;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prediction pattern for a cache key
|
||||||
|
*/
|
||||||
|
final class PredictionPattern
|
||||||
|
{
|
||||||
|
/** @var array<array{timestamp: Timestamp, context: array}> */
|
||||||
|
private array $accesses = [];
|
||||||
|
|
||||||
|
/** @var array<CacheKey> */
|
||||||
|
private array $dependencies = [];
|
||||||
|
|
||||||
|
private mixed $warmingCallback = null;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly CacheKey $key
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function recordAccess(Timestamp $timestamp, array $context = []): void
|
||||||
|
{
|
||||||
|
$this->accesses[] = [
|
||||||
|
'timestamp' => $timestamp,
|
||||||
|
'context' => $context,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Keep only recent accesses to prevent memory bloat
|
||||||
|
$cutoff = $timestamp->subtract(Duration::fromHours(168)); // 1 week
|
||||||
|
$this->accesses = array_filter(
|
||||||
|
$this->accesses,
|
||||||
|
fn($access) => $access['timestamp']->isAfter($cutoff)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addDependency(CacheKey $dependentKey): void
|
||||||
|
{
|
||||||
|
$this->dependencies[] = $dependentKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setWarmingCallback(callable $callback): void
|
||||||
|
{
|
||||||
|
$this->warmingCallback = $callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getKey(): CacheKey
|
||||||
|
{
|
||||||
|
return $this->key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRecentAccesses(int $hours): array
|
||||||
|
{
|
||||||
|
$cutoff = Timestamp::now()->subtract(Duration::fromHours($hours));
|
||||||
|
|
||||||
|
return array_filter(
|
||||||
|
$this->accesses,
|
||||||
|
fn($access) => $access['timestamp']->isAfter($cutoff)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDependencies(): array
|
||||||
|
{
|
||||||
|
return $this->dependencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getWarmingCallback(): ?callable
|
||||||
|
{
|
||||||
|
return $this->warmingCallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/Framework/Cache/Strategies/WarmingJob.php
Normal file
16
src/Framework/Cache/Strategies/WarmingJob.php
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Framework\Cache\Strategies;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Active warming job
|
||||||
|
*/
|
||||||
|
final readonly class WarmingJob
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public CacheKey $key,
|
||||||
|
public mixed $callback,
|
||||||
|
public string $reason,
|
||||||
|
public Timestamp $startTime
|
||||||
|
) {}
|
||||||
|
}
|
||||||
16
src/Framework/Cache/Strategies/WarmingResult.php
Normal file
16
src/Framework/Cache/Strategies/WarmingResult.php
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Framework\Cache\Strategies;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Warming operation result
|
||||||
|
*/
|
||||||
|
final readonly class WarmingResult
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public CacheKey $key,
|
||||||
|
public bool $successful,
|
||||||
|
public Duration $duration,
|
||||||
|
public string $reason
|
||||||
|
) {}
|
||||||
|
}
|
||||||
16
src/Framework/Cache/Strategies/WriteOperation.php
Normal file
16
src/Framework/Cache/Strategies/WriteOperation.php
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Framework\Cache\Strategies;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write operation tracking
|
||||||
|
*/
|
||||||
|
final readonly class WriteOperation
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public CacheKey $key,
|
||||||
|
public int $valueSize,
|
||||||
|
public Duration $writeTime,
|
||||||
|
public Timestamp $timestamp
|
||||||
|
) {}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user