# PHP Implementation Guide

## Overview

This guide provides step-by-step instructions for implementing the EventBookings Shopify app in PHP, with examples for both Laravel and vanilla PHP approaches. The implementation maintains feature parity with the original Django version while leveraging PHP ecosystem best practices.

## Framework Recommendations

### Laravel (Recommended)
- **Pros**: Rich ecosystem, built-in features (Eloquent, Artisan, Blade), excellent documentation
- **Cons**: Learning curve for non-Laravel developers
- **Best for**: Full-featured implementation with rapid development

### Symfony
- **Pros**: Modular architecture, enterprise-grade, flexible
- **Cons**: Steeper learning curve, more configuration required
- **Best for**: Large-scale applications with specific architecture requirements

### Vanilla PHP with Libraries
- **Pros**: Maximum control, lighter footprint, framework-agnostic
- **Cons**: More boilerplate code, manual dependency management
- **Best for**: Custom implementations or existing PHP codebases

## Quick Start Implementation

### Option 1: Laravel Implementation

#### Step 1: Project Setup

```bash
# Create new Laravel project
composer create-project laravel/laravel eventbookings-shopify-app

cd eventbookings-shopify-app

# Install required packages
composer require guzzlehttp/guzzle
composer require predis/predis

# Install development tools
composer require --dev phpunit/phpunit
```

#### Step 2: Environment Configuration

**File: `.env`**
```env
APP_NAME="EventBookings Shopify App"
APP_ENV=local
APP_KEY=base64:YOUR_APP_KEY
APP_DEBUG=true
APP_URL=http://localhost:8000

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=eventbookings_shopify
DB_USERNAME=root
DB_PASSWORD=

# Shopify App Configuration
SHOPIFY_CLIENT_ID=your_shopify_client_id
SHOPIFY_CLIENT_SECRET=your_shopify_client_secret
SHOPIFY_REDIRECT_URI=https://your-app-domain.com/auth/shopify/callback

# EventBookings API Configuration
EVENTBOOKINGS_IDENTITY_URL=https://identity.eventbookings.com
EVENTBOOKINGS_API_URL=https://api-rto.eventbookings.com/v3

# Cache Configuration
CACHE_DRIVER=redis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
```

#### Step 3: Database Migrations

```bash
# Create migrations
php artisan make:migration create_shopify_stores_table
php artisan make:migration create_store_settings_table
php artisan make:migration create_event_toggles_table
```

**Migration Files:**

```php
// database/migrations/2024_01_01_000001_create_shopify_stores_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up()
    {
        Schema::create('shopify_stores', function (Blueprint $table) {
            $table->id();
            $table->string('shop_domain')->unique();
            $table->text('access_token');
            $table->timestamps();

            $table->index('shop_domain');
        });
    }

    public function down()
    {
        Schema::dropIfExists('shopify_stores');
    }
};
```

```php
// database/migrations/2024_01_01_000002_create_store_settings_table.php
return new class extends Migration
{
    public function up()
    {
        Schema::create('store_settings', function (Blueprint $table) {
            $table->id();
            $table->foreignId('store_id')->constrained('shopify_stores')->onDelete('cascade');
            $table->string('client_id')->default('');
            $table->text('client_secret')->default('');
            $table->longText('access_token')->default('');
            $table->string('org_uuid')->default('');
            $table->timestamp('token_expires_at')->nullable();
            $table->boolean('is_connected')->default(false);
            $table->text('connection_error')->default('');
            $table->timestamps();

            $table->unique('store_id');
        });
    }

    public function down()
    {
        Schema::dropIfExists('store_settings');
    }
};
```

```php
// database/migrations/2024_01_01_000003_create_event_toggles_table.php
return new class extends Migration
{
    public function up()
    {
        Schema::create('event_toggles', function (Blueprint $table) {
            $table->id();
            $table->foreignId('store_id')->constrained('shopify_stores')->onDelete('cascade');
            $table->string('event_uuid');
            $table->boolean('show_event')->default(true);
            $table->boolean('featured_event')->default(false);
            $table->timestamps();

            $table->unique(['store_id', 'event_uuid']);
            $table->index(['store_id', 'show_event']);
            $table->index(['store_id', 'featured_event']);
        });
    }

    public function down()
    {
        Schema::dropIfExists('event_toggles');
    }
};
```

```bash
# Run migrations
php artisan migrate
```

#### Step 4: Model Creation

```bash
# Create models
php artisan make:model ShopifyStore
php artisan make:model StoreSettings
php artisan make:model EventToggle
```

**Model Files:**

```php
// app/Models/ShopifyStore.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Facades\Crypt;

class ShopifyStore extends Model
{
    protected $fillable = [
        'shop_domain',
        'access_token'
    ];

    protected $hidden = [
        'access_token'
    ];

    public function storeSettings(): HasOne
    {
        return $this->hasOne(StoreSettings::class, 'store_id');
    }

    public function eventToggles(): HasMany
    {
        return $this->hasMany(EventToggle::class, 'store_id');
    }

    public function setAccessTokenAttribute($value): void
    {
        $this->attributes['access_token'] = Crypt::encryptString($value);
    }

    public function getAccessTokenAttribute($value): ?string
    {
        try {
            return Crypt::decryptString($value);
        } catch (\Exception $e) {
            \Log::error('Failed to decrypt access token', [
                'store_id' => $this->id,
                'error' => $e->getMessage()
            ]);
            return null;
        }
    }
}
```

```php
// app/Models/StoreSettings.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Facades\Crypt;
use Carbon\Carbon;

class StoreSettings extends Model
{
    protected $fillable = [
        'store_id',
        'client_id',
        'client_secret',
        'access_token',
        'org_uuid',
        'token_expires_at',
        'is_connected',
        'connection_error'
    ];

    protected $hidden = [
        'client_secret',
        'access_token'
    ];

    protected $casts = [
        'token_expires_at' => 'datetime',
        'is_connected' => 'boolean',
    ];

    public function store(): BelongsTo
    {
        return $this->belongsTo(ShopifyStore::class, 'store_id');
    }

    // Encryption/Decryption methods
    public function setClientSecretAttribute($value): void
    {
        $this->attributes['client_secret'] = $value ? Crypt::encryptString($value) : '';
    }

    public function getClientSecretAttribute($value): string
    {
        if (!$value) return '';
        try {
            return Crypt::decryptString($value);
        } catch (\Exception $e) {
            return '';
        }
    }

    public function setAccessTokenAttribute($value): void
    {
        $this->attributes['access_token'] = $value ? Crypt::encryptString($value) : '';
    }

    public function getAccessTokenAttribute($value): string
    {
        if (!$value) return '';
        try {
            return Crypt::decryptString($value);
        } catch (\Exception $e) {
            return '';
        }
    }

    // Helper methods
    public function isTokenExpired(): bool
    {
        return $this->token_expires_at && $this->token_expires_at->isPast();
    }

    public function isTokenExpiringSoon(int $minutes = 5): bool
    {
        return $this->token_expires_at &&
               $this->token_expires_at->diffInMinutes(now()) <= $minutes;
    }

    public function hasValidCredentials(): bool
    {
        return !empty($this->client_id) && !empty($this->client_secret);
    }
}
```

```php
// app/Models/EventToggle.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class EventToggle extends Model
{
    protected $fillable = [
        'store_id',
        'event_uuid',
        'show_event',
        'featured_event'
    ];

    protected $casts = [
        'show_event' => 'boolean',
        'featured_event' => 'boolean',
    ];

    public function store(): BelongsTo
    {
        return $this->belongsTo(ShopifyStore::class, 'store_id');
    }

    // Scopes
    public function scopeVisible($query)
    {
        return $query->where('show_event', true);
    }

    public function scopeFeatured($query)
    {
        return $query->where('featured_event', true);
    }

    public function scopeForStore($query, int $storeId)
    {
        return $query->where('store_id', $storeId);
    }

    // Static helper methods
    public static function getEventSettings(int $storeId, string $eventUuid): ?self
    {
        return self::where('store_id', $storeId)
                   ->where('event_uuid', $eventUuid)
                   ->first();
    }

    public static function setEventVisibility(int $storeId, string $eventUuid, bool $visible): self
    {
        return self::updateOrCreate(
            ['store_id' => $storeId, 'event_uuid' => $eventUuid],
            ['show_event' => $visible]
        );
    }

    public static function setEventFeatured(int $storeId, string $eventUuid, bool $featured): self
    {
        return self::updateOrCreate(
            ['store_id' => $storeId, 'event_uuid' => $eventUuid],
            ['featured_event' => $featured]
        );
    }
}
```

#### Step 5: Service Classes

```bash
# Create service classes
php artisan make:class Services/EventBookingsApi
php artisan make:class Services/ShopifyAuth
php artisan make:class Services/TokenManager
```

**Service implementations are detailed in [API Integrations](03-api-integrations.md)**

#### Step 6: Controllers

```bash
# Create controllers
php artisan make:controller ShopifyAuthController
php artisan make:controller DashboardController
php artisan make:controller WidgetController
```

**Controller implementations are detailed in [Dashboard & Admin](06-dashboard-admin.md) and [Widget System](05-widget-system.md)**

#### Step 7: Middleware

```bash
# Create middleware
php artisan make:middleware VerifyShopifyAuth
php artisan make:middleware CorsMiddleware
```

**Middleware implementations are detailed in [Authentication & Security](04-authentication-security.md)**

#### Step 8: Routes

**File: `routes/web.php`**
```php
<?php

use App\Http\Controllers\ShopifyAuthController;
use App\Http\Controllers\DashboardController;
use Illuminate\Support\Facades\Route;

// Shopify OAuth routes
Route::get('/auth/shopify/install', [ShopifyAuthController::class, 'install'])->name('auth.install');
Route::get('/auth/shopify/callback', [ShopifyAuthController::class, 'callback'])->name('auth.callback');

// Dashboard routes (protected by Shopify auth)
Route::middleware(['auth.shopify'])->group(function () {
    Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard');
    Route::post('/dashboard', [DashboardController::class, 'handleAction'])->name('dashboard.action');
});

// Root redirect
Route::get('/', function () {
    return redirect()->route('auth.install');
});
```

**File: `routes/api.php`**
```php
<?php

use App\Http\Controllers\WidgetController;
use Illuminate\Support\Facades\Route;

// Widget API routes (CORS enabled)
Route::middleware(['cors'])->group(function () {
    Route::get('/widgets/events.js', [WidgetController::class, 'eventsWidget']);
    Route::get('/widgets/featured.js', [WidgetController::class, 'featuredWidget']);
    Route::get('/api/events', [WidgetController::class, 'eventsApi']);
});
```

#### Step 9: Configuration

**File: `config/shopify.php`**
```php
<?php

return [
    'client_id' => env('SHOPIFY_CLIENT_ID'),
    'client_secret' => env('SHOPIFY_CLIENT_SECRET'),
    'redirect_uri' => env('SHOPIFY_REDIRECT_URI'),
    'scopes' => [
        'read_themes',
        'write_themes',
    ],
];
```

**File: `config/eventbookings.php`**
```php
<?php

return [
    'identity_url' => env('EVENTBOOKINGS_IDENTITY_URL', 'https://identity.eventbookings.com'),
    'api_url' => env('EVENTBOOKINGS_API_URL', 'https://api-rto.eventbookings.com/v3'),
    'timeout' => env('EVENTBOOKINGS_TIMEOUT', 30),
];
```

### Option 2: Vanilla PHP Implementation

For developers preferring a framework-less approach, here's a minimal implementation structure:

#### Directory Structure

```
eventbookings-shopify-app/
├── public/
│   ├── index.php
│   ├── dashboard.php
│   ├── widgets/
│   │   ├── events.php
│   │   └── featured.php
│   └── assets/
├── src/
│   ├── Config/
│   │   └── Database.php
│   ├── Models/
│   │   ├── ShopifyStore.php
│   │   ├── StoreSettings.php
│   │   └── EventToggle.php
│   ├── Services/
│   │   ├── EventBookingsApi.php
│   │   ├── ShopifyAuth.php
│   │   └── TokenManager.php
│   ├── Controllers/
│   │   ├── DashboardController.php
│   │   └── WidgetController.php
│   └── Utils/
│       ├── Database.php
│       ├── Security.php
│       └── Http.php
├── templates/
│   ├── dashboard.php
│   └── widgets/
├── vendor/
├── composer.json
└── .env
```

#### Composer Setup

**File: `composer.json`**
```json
{
    "name": "eventbookings/shopify-app",
    "description": "EventBookings Shopify Integration",
    "type": "project",
    "require": {
        "php": "^8.0",
        "guzzlehttp/guzzle": "^7.0",
        "vlucas/phpdotenv": "^5.0",
        "predis/predis": "^2.0"
    },
    "autoload": {
        "psr-4": {
            "EventBookings\\": "src/"
        }
    }
}
```

#### Basic Bootstrap

**File: `public/index.php`**
```php
<?php

require_once __DIR__ . '/../vendor/autoload.php';

use Dotenv\Dotenv;
use EventBookings\Controllers\DashboardController;
use EventBookings\Controllers\WidgetController;
use EventBookings\Services\ShopifyAuth;

// Load environment variables
$dotenv = Dotenv::createImmutable(__DIR__ . '/..');
$dotenv->load();

// Simple router
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$method = $_SERVER['REQUEST_METHOD'];

// Routes
switch ($uri) {
    case '/':
        header('Location: /auth/install');
        exit;

    case '/auth/install':
        $auth = new ShopifyAuth();
        $auth->handleInstall();
        break;

    case '/auth/callback':
        $auth = new ShopifyAuth();
        $auth->handleCallback();
        break;

    case '/dashboard':
        $controller = new DashboardController();
        if ($method === 'POST') {
            $controller->handleAction();
        } else {
            $controller->index();
        }
        break;

    case '/widgets/events.js':
        $controller = new WidgetController();
        $controller->eventsWidget();
        break;

    case '/widgets/featured.js':
        $controller = new WidgetController();
        $controller->featuredWidget();
        break;

    case '/api/events':
        $controller = new WidgetController();
        $controller->eventsApi();
        break;

    default:
        http_response_code(404);
        echo 'Not Found';
        break;
}
```

#### Database Configuration

**File: `src/Config/Database.php`**
```php
<?php

namespace EventBookings\Config;

use PDO;
use PDOException;

class Database
{
    private static $instance = null;
    private $connection;

    private function __construct()
    {
        try {
            $dsn = sprintf(
                'mysql:host=%s;dbname=%s;charset=utf8mb4',
                $_ENV['DB_HOST'],
                $_ENV['DB_DATABASE']
            );

            $this->connection = new PDO(
                $dsn,
                $_ENV['DB_USERNAME'],
                $_ENV['DB_PASSWORD'],
                [
                    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
                    PDO::ATTR_EMULATE_PREPARES => false,
                ]
            );
        } catch (PDOException $e) {
            throw new \RuntimeException('Database connection failed: ' . $e->getMessage());
        }
    }

    public static function getInstance(): self
    {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    public function getConnection(): PDO
    {
        return $this->connection;
    }

    public static function query(string $sql, array $params = []): \PDOStatement
    {
        $db = self::getInstance()->getConnection();
        $stmt = $db->prepare($sql);
        $stmt->execute($params);
        return $stmt;
    }

    public static function fetchOne(string $sql, array $params = []): ?array
    {
        $stmt = self::query($sql, $params);
        $result = $stmt->fetch();
        return $result ?: null;
    }

    public static function fetchAll(string $sql, array $params = []): array
    {
        $stmt = self::query($sql, $params);
        return $stmt->fetchAll();
    }

    public static function execute(string $sql, array $params = []): bool
    {
        $stmt = self::query($sql, $params);
        return $stmt->rowCount() > 0;
    }

    public static function lastInsertId(): string
    {
        return self::getInstance()->getConnection()->lastInsertId();
    }
}
```

#### Model Base Class

**File: `src/Models/BaseModel.php`**
```php
<?php

namespace EventBookings\Models;

use EventBookings\Config\Database;

abstract class BaseModel
{
    protected $table;
    protected $primaryKey = 'id';
    protected $fillable = [];
    protected $hidden = [];
    protected $casts = [];

    public function __construct(array $attributes = [])
    {
        $this->fill($attributes);
    }

    public function fill(array $attributes): void
    {
        foreach ($attributes as $key => $value) {
            if (in_array($key, $this->fillable)) {
                $this->$key = $value;
            }
        }
    }

    public function save(): bool
    {
        $attributes = $this->getAttributes();

        if (isset($this->{$this->primaryKey})) {
            return $this->update($attributes);
        } else {
            return $this->insert($attributes);
        }
    }

    private function insert(array $attributes): bool
    {
        $columns = implode(', ', array_keys($attributes));
        $placeholders = ':' . implode(', :', array_keys($attributes));

        $sql = "INSERT INTO {$this->table} ($columns) VALUES ($placeholders)";
        $result = Database::execute($sql, $attributes);

        if ($result) {
            $this->{$this->primaryKey} = Database::lastInsertId();
        }

        return $result;
    }

    private function update(array $attributes): bool
    {
        $setParts = [];
        foreach (array_keys($attributes) as $column) {
            $setParts[] = "$column = :$column";
        }
        $setClause = implode(', ', $setParts);

        $sql = "UPDATE {$this->table} SET $setClause WHERE {$this->primaryKey} = :{$this->primaryKey}";
        $attributes[$this->primaryKey] = $this->{$this->primaryKey};

        return Database::execute($sql, $attributes);
    }

    public static function find(int $id): ?static
    {
        $instance = new static();
        $sql = "SELECT * FROM {$instance->table} WHERE {$instance->primaryKey} = :id LIMIT 1";
        $row = Database::fetchOne($sql, ['id' => $id]);

        if ($row) {
            return new static($row);
        }

        return null;
    }

    public static function where(string $column, $value): array
    {
        $instance = new static();
        $sql = "SELECT * FROM {$instance->table} WHERE $column = :value";
        $rows = Database::fetchAll($sql, ['value' => $value]);

        return array_map(function($row) {
            return new static($row);
        }, $rows);
    }

    private function getAttributes(): array
    {
        $attributes = [];
        foreach ($this->fillable as $attribute) {
            if (isset($this->$attribute)) {
                $attributes[$attribute] = $this->$attribute;
            }
        }
        return $attributes;
    }

    public function toArray(): array
    {
        $array = $this->getAttributes();

        // Remove hidden attributes
        foreach ($this->hidden as $hidden) {
            unset($array[$hidden]);
        }

        return $array;
    }
}
```

## Testing Implementation

### Laravel Testing

```bash
# Create test classes
php artisan make:test EventBookingsApiTest
php artisan make:test DashboardControllerTest
php artisan make:test WidgetControllerTest
```

**Example Test:**
```php
<?php

namespace Tests\Feature;

use Tests\TestCase;
use App\Models\ShopifyStore;
use App\Models\StoreSettings;
use Illuminate\Foundation\Testing\RefreshDatabase;

class DashboardControllerTest extends TestCase
{
    use RefreshDatabase;

    public function test_dashboard_requires_authentication(): void
    {
        $response = $this->get('/dashboard');
        $response->assertStatus(401);
    }

    public function test_dashboard_loads_for_authenticated_store(): void
    {
        $store = ShopifyStore::factory()->create();
        $store->storeSettings()->create([
            'store_id' => $store->id
        ]);

        $response = $this->get('/dashboard?shop=' . $store->shop_domain);
        $response->assertStatus(200);
        $response->assertViewIs('dashboard');
    }

    public function test_settings_save_with_valid_credentials(): void
    {
        $store = ShopifyStore::factory()->create();
        $store->storeSettings()->create(['store_id' => $store->id]);

        $response = $this->post('/dashboard?shop=' . $store->shop_domain, [
            'action' => 'save',
            'client_id' => 'test_client_id',
            'client_secret' => 'test_client_secret',
        ]);

        $response->assertRedirect();
        $this->assertDatabaseHas('store_settings', [
            'store_id' => $store->id,
            'client_id' => 'test_client_id'
        ]);
    }
}
```

### PHPUnit Configuration

**File: `phpunit.xml`**
```xml
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
         bootstrap="vendor/autoload.php"
         colors="true">
    <testsuites>
        <testsuite name="Unit">
            <directory suffix="Test.php">./tests/Unit</directory>
        </testsuite>
        <testsuite name="Feature">
            <directory suffix="Test.php">./tests/Feature</directory>
        </testsuite>
    </testsuites>
    <php>
        <env name="APP_ENV" value="testing"/>
        <env name="DB_DATABASE" value="eventbookings_testing"/>
    </php>
</phpunit>
```

## Performance Optimization

### Caching Strategy

```php
// Example caching implementation
use Illuminate\Support\Facades\Cache;

class EventsService
{
    public function getEventsForWidget(string $shopDomain, string $type = 'all'): array
    {
        $cacheKey = "events_widget_{$shopDomain}_{$type}";

        return Cache::remember($cacheKey, 300, function() use ($shopDomain, $type) {
            // Fetch from API
            return $this->fetchEventsFromApi($shopDomain, $type);
        });
    }

    public function clearEventsCache(string $shopDomain): void
    {
        Cache::forget("events_widget_{$shopDomain}_all");
        Cache::forget("events_widget_{$shopDomain}_featured");
    }
}
```

### Database Optimization

```php
// Use eager loading to prevent N+1 queries
$stores = ShopifyStore::with(['storeSettings', 'eventToggles'])->get();

// Use database transactions for data consistency
DB::transaction(function() use ($storeId, $settings) {
    StoreSettings::where('store_id', $storeId)->update($settings);
    Cache::forget("store_settings_{$storeId}");
});

// Index optimization
Schema::table('event_toggles', function (Blueprint $table) {
    $table->index(['store_id', 'show_event']);
    $table->index(['store_id', 'featured_event']);
});
```

## Deployment Considerations

### Production Configuration

```php
// config/app.php
'env' => env('APP_ENV', 'production'),
'debug' => (bool) env('APP_DEBUG', false),

// Use appropriate session driver
'SESSION_DRIVER' => 'redis',

// Enable HTTPS in production
'FORCE_HTTPS' => env('FORCE_HTTPS', true),
```

### Security Checklist

- [ ] Enable HTTPS for all endpoints
- [ ] Configure proper CORS headers
- [ ] Set up rate limiting
- [ ] Enable SQL injection protection
- [ ] Implement CSRF protection
- [ ] Use encrypted token storage
- [ ] Configure secure session handling
- [ ] Set up proper error logging

### Monitoring Setup

```php
// Example logging configuration
'channels' => [
    'api' => [
        'driver' => 'daily',
        'path' => storage_path('logs/api.log'),
        'level' => 'info',
    ],
    'security' => [
        'driver' => 'daily',
        'path' => storage_path('logs/security.log'),
        'level' => 'warning',
    ],
],
```

This implementation guide provides a solid foundation for developing the EventBookings Shopify app in PHP while maintaining all the features and functionality of the original Django implementation.