# Workflow API - Backend Coding Agent

You are a backend coding agent for this PHP REST API. Follow every convention below precisely when creating or modifying code.

---

## Stack

- **Framework**: Slim Framework v3 (via `linways/slim ^1.0.0`)
- **Language**: PHP
- **Auth**: JWT (HS512) via `linways/linways-auth`
- **Validation**: `respect/validation ^1.1`
- **Migrations**: Phinx (`phinx.yml`)
- **Testing**: PHPUnit 7 with Faker

---

## Directory Structure

```
src/com/linways/
├── api/v1/
│   ├── {module}/
│   │   ├── controller/
│   │   │   └── {Module}Controller.php
│   │   └── routes.php
│   ├── BaseController.php          ← extend this for all API controllers
│   └── routes.php                  ← register module route groups here
├── core/
│   ├── service/
│   │   ├── BaseService.php         ← extend this for all services
│   │   └── {Module}Service.php
│   ├── dto/
│   │   └── {Module}.php
│   ├── request/
│   │   └── Search{Module}Request.php
│   ├── mapper/
│   │   └── {Module}ServiceMapper.php
│   ├── exception/
│   │   └── {Module}Exception.php
│   └── constant/
│       └── {Module}Constant.php
db/migrations/                      ← Phinx migration files
test/unit/service/                  ← PHPUnit service tests
```

---

## Namespace Convention

```
com\linways\wm\api\v1\{module}\controller\{Module}Controller
com\linways\wm\core\service\{Module}Service
com\linways\wm\core\dto\{Module}
com\linways\wm\core\mapper\{Module}ServiceMapper
com\linways\wm\core\exception\{Module}Exception
com\linways\wm\core\request\Search{Module}Request
```

---

## Naming Conventions

| Type | Convention | Example |
|---|---|---|
| Classes | PascalCase | `WorkflowService` |
| Methods | camelCase | `saveWorkflow()` |
| Properties | camelCase | `workFlowId` |
| DB columns | snake_case | `wm_workflow_id` |
| DB tables | `wm_` prefix + snake_case (this project only) | `wm_workflow` |
| Constants | UPPER_SNAKE_CASE | `CREATE_WORKFLOW_FAILED` |
| Files | PascalCase matching class name | `WorkflowController.php` |

---

## 1. Creating a New Module — Step-by-Step

### Step 1: DTO (`src/com/linways/core/dto/{Module}.php`)

```php
<?php
namespace com\linways\wm\core\dto;

use com\linways\base\dto\BaseDTO;

class {Module} extends BaseDTO {
    public $id;
    public $name;
    public $description;
    public $isActive = 1;
    // JSON fields stored as string in DB, decoded to object in mapper
    public $rule;
    public $createdDate;
    public $updatedDate;
}
```

### Step 2: Exception (`src/com/linways/core/exception/{Module}Exception.php`)

```php
<?php
namespace com\linways\wm\core\exception;

use com\linways\base\exception\CoreException;

class {Module}Exception extends CoreException {
    const CREATE_{MODULE}_FAILED = [1, "{Module} creation failed"];
    const UPDATE_{MODULE}_FAILED = [2, "{Module} update failed"];
    const DELETE_{MODULE}_FAILED = [3, "{Module} deletion failed"];
    const {MODULE}_NOT_FOUND    = [4, "{Module} not found"];
    const INVALID_{MODULE}      = [5, "Invalid {module} data"];
}
```

### Step 3: Mapper (`src/com/linways/core/mapper/{Module}ServiceMapper.php`)

```php
<?php
namespace com\linways\wm\core\mapper;

use com\linways\base\mapper\ResultMap;
use com\linways\base\mapper\Result;

class {Module}ServiceMapper {
    public static function get{Module}ListMapper() {
        return (new ResultMap())
            ->setClass(\com\linways\wm\core\dto\{Module}::class)
            ->addResult(new Result("id",          "id",          "INT"))
            ->addResult(new Result("name",        "name"))
            ->addResult(new Result("description", "description"))
            ->addResult(new Result("isActive",    "is_active",   "INT"))
            ->addResult(new Result("rule",        "rule",        "OBJECT_FROM_JSON"))
            ->addResult(new Result("createdDate", "created_date"));
    }
}
```

**Mapper type constants:**
- `"INT"` — cast to integer
- `"OBJECT_FROM_JSON"` — JSON decode to stdClass
- `"OBJECT_ARRAY"` — JSON decode to array
- *(omit)* — string passthrough

### Step 4: Request Object (`src/com/linways/core/request/Search{Module}Request.php`)

```php
<?php
namespace com\linways\wm\core\request;

use com\linways\base\request\BaseRequest;

class Search{Module}Request extends BaseRequest {
    public $id;
    public $name;
    public $isActive;
    public $limit  = 20;
    public $offset = 0;
}
```

### Step 5: Service (`src/com/linways/core/service/{Module}Service.php`)

```php
<?php
namespace com\linways\wm\core\service;

use com\linways\base\util\MakeSingletonTrait;
use com\linways\wm\core\dto\{Module};
use com\linways\wm\core\exception\{Module}Exception;
use com\linways\wm\core\mapper\{Module}ServiceMapper;
use com\linways\wm\core\request\Search{Module}Request;
use Respect\Validation\Validator as v;

class {Module}Service extends BaseService {
    use MakeSingletonTrait;

    private function __construct() {}

    /**
     * Create or update a {module}.
     */
    public function save{Module}({Module} ${module}): {Module} {
        // Validate
        v::attribute("name", v::stringType()->notEmpty())
          ->assert(${module});

        return empty(${module}->id)
            ? $this->create{Module}(${module})
            : $this->update{Module}(${module});
    }

    private function create{Module}({Module} ${module}): {Module} {
        ${module} = $this->realEscapeObject(${module});
        $query = "INSERT INTO `wm_{module}` (`name`, `description`, `is_active`, `created_by`)
                  VALUES ('{${module}->name}', '{${module}->description}', 1, '$GLOBALS[userId]')";
        try {
            ${module}->id = $this->executeQuery($query);
        } catch (\Exception $e) {
            throw new {Module}Exception({Module}Exception::CREATE_{MODULE}_FAILED);
        }
        return ${module};
    }

    private function update{Module}({Module} ${module}): {Module} {
        ${module} = $this->realEscapeObject(${module});
        $query = "UPDATE `wm_{module}`
                  SET `name` = '{${module}->name}',
                      `description` = '{${module}->description}',
                      `updated_by` = '$GLOBALS[userId]'
                  WHERE `id` = {${module}->id}";
        try {
            $this->executeQuery($query);
        } catch (\Exception $e) {
            throw new {Module}Exception({Module}Exception::UPDATE_{MODULE}_FAILED);
        }
        return ${module};
    }

    public function search{Module}s(Search{Module}Request $request): array {
        $request = $this->realEscapeObject($request);
        $conditions = ["1=1"];
        if (!empty($request->id))       $conditions[] = "w.id = {$request->id}";
        if (!empty($request->name))     $conditions[] = "w.name LIKE '%{$request->name}%'";
        if (isset($request->isActive))  $conditions[] = "w.is_active = {$request->isActive}";
        $where = implode(" AND ", $conditions);

        $query = "SELECT w.id, w.name, w.description, w.is_active, w.created_date
                  FROM `wm_{module}` w
                  WHERE {$where}
                  ORDER BY w.id DESC
                  LIMIT {$request->offset}, {$request->limit}";

        return $this->executeQueryForList($query, {Module}ServiceMapper::get{Module}ListMapper());
    }

    public function delete{Module}(int $id): void {
        $query = "UPDATE `wm_{module}` SET `is_active` = 0 WHERE `id` = {$id}";
        try {
            $this->executeQuery($query);
        } catch (\Exception $e) {
            throw new {Module}Exception({Module}Exception::DELETE_{MODULE}_FAILED);
        }
    }
}
```

**Key BaseService query methods:**
- `$this->executeQuery($query)` — INSERT (returns last insert ID), UPDATE, DELETE
- `$this->executeQueryForList($query, $mapper)` — SELECT returning array of DTOs
- `$this->executeQueryForObject($query, $mapper)` — SELECT returning single DTO
- `$this->realEscapeObject($object)` — escape all string properties (ALWAYS call before building queries)

### Step 6: Controller (`src/com/linways/api/v1/{module}/controller/{Module}Controller.php`)

```php
<?php
namespace com\linways\wm\api\v1\{module}\controller;

use com\linways\wm\api\v1\BaseController;
use com\linways\wm\core\dto\{Module};
use com\linways\wm\core\request\Search{Module}Request;
use com\linways\wm\core\service\{Module}Service;
use Linways\Slim\Utils\ResponseUtils;
use Slim\Http\Request;
use Slim\Http\Response;

class {Module}Controller extends BaseController {

    // Declare required permissions per method: public $permissions_{methodName} = ['PERMISSION_KEY']
    public $permissions_save{Module}   = ['{MODULE}_WRITE'];
    public $permissions_search{Module}s = ['{MODULE}_READ'];
    public $permissions_delete{Module} = ['{MODULE}_DELETE'];

    protected function save{Module}(Request $request, Response $response) {
        try {
            $body   = $request->getParsedBody();
            $id     = $request->getAttribute('id');    // from route param
            ${module} = new {Module}();
            ${module}->id          = $id ?? null;
            ${module}->name        = $body['name']        ?? null;
            ${module}->description = $body['description'] ?? null;

            $result = {Module}Service::getInstance()->save{Module}(${module});
            return ResponseUtils::result($response, $result);
        } catch (\Exception $e) {
            return ResponseUtils::fault($response, $e);
        }
    }

    protected function search{Module}s(Request $request, Response $response) {
        try {
            $params  = $request->getQueryParams();
            $req     = new Search{Module}Request();
            $req->id       = $params['id']       ?? null;
            $req->name     = $params['name']      ?? null;
            $req->isActive = $params['isActive']  ?? 1;
            $req->limit    = $params['limit']     ?? 20;
            $req->offset   = $params['offset']    ?? 0;

            $result = {Module}Service::getInstance()->search{Module}s($req);
            return ResponseUtils::result($response, $result);
        } catch (\Exception $e) {
            return ResponseUtils::fault($response, $e);
        }
    }

    protected function delete{Module}(Request $request, Response $response) {
        try {
            $id = (int) $request->getAttribute('id');
            {Module}Service::getInstance()->delete{Module}($id);
            return ResponseUtils::result($response, null);
        } catch (\Exception $e) {
            return ResponseUtils::fault($response, $e);
        }
    }
}
```

### Step 7: Routes (`src/com/linways/api/v1/{module}/routes.php`)

```php
<?php
use Slim\App;

return function(App $app) {
    $app->get('[/]',         '{Module}Controller:search{Module}s');
    $app->post('[/]',        '{Module}Controller:save{Module}');
    $app->put('/{id}[/]',   '{Module}Controller:save{Module}');
    $app->delete('/{id}[/]','{Module}Controller:delete{Module}');
};
```

### Step 8: Register Routes (`src/com/linways/api/v1/routes.php`)

Add to the existing route group file:

```php
$app->group('/{module}', require __DIR__ . '/{module}/routes.php');
```

### Step 9: Register Controller (`bootstrap/controllers.php`)

```php
$container['{Module}Controller'] = function ($c) {
    return new com\linways\wm\api\v1\{module}\controller\{Module}Controller();
};
```

---

## 2. Database Migrations

Create a Phinx migration in `db/migrations/`:

```php
<?php
use Phinx\Migration\AbstractMigration;

class Create{Module}Table extends AbstractMigration {
    public function change() {
        $this->execute("CREATE TABLE `wm_{module}` (
            `id` INT NOT NULL AUTO_INCREMENT,
            `name` VARCHAR(200) NOT NULL,
            `description` TEXT,
            `rule` LONGTEXT COMMENT 'JSON',
            `is_active` INT NOT NULL DEFAULT 1,
            `created_by` VARCHAR(100),
            `updated_by` VARCHAR(100),
            `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
            `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
            PRIMARY KEY (`id`)
        );");
    }
}
```

Run migrations:
```bash
php db/manager.php migrate
```

---

## 3. Testing

### Service Test (`test/unit/service/{Module}ServiceTest.php`)

```php
<?php
namespace test\unit\service;

use test\unit\WorkFlowTestCase;
use com\linways\wm\core\dto\{Module};
use com\linways\wm\core\service\{Module}Service;
use com\linways\wm\core\request\Search{Module}Request;

class {Module}ServiceTest extends WorkFlowTestCase {

    private ${module}Id;

    public function testCreate{Module}() {
        ${module}       = new {Module}();
        ${module}->name = $this->faker->word;

        $result = {Module}Service::getInstance()->save{Module}(${module});

        $this->assertNotEmpty($result->id);
        $this->{module}Id = $result->id;
        return $result->id;
    }

    /** @depends testCreate{Module} */
    public function testSearch{Module}s($id) {
        $req     = new Search{Module}Request();
        $req->id = $id;
        $results = {Module}Service::getInstance()->search{Module}s($req);
        $this->assertNotEmpty($results);
        $this->assertEquals($id, $results[0]->id);
    }
}
```

Run tests:
```bash
./vendor/bin/phpunit test/unit/service/{Module}ServiceTest.php
```

---

## 4. Key Patterns & Rules

### Authentication Context (always available in services/controllers)
```php
$GLOBALS['userId']       // Current user ID
$GLOBALS['userType']     // STAFF | STUDENT | ADMIN
$GLOBALS['role']         // Role string
$GLOBALS['departmentId'] // Department context
$GLOBALS['collegeCode']  // Institution code
```

### Permission Declarations on Controllers
```php
// Method name must match exactly: permissions_{methodName}
public $permissions_saveWorkflow = ['WORKFLOW_WRITE'];
```
- If `$isPermissionsRequired = false` is set on the controller, permissions are skipped.

### Always Escape Before Queries
```php
$obj = $this->realEscapeObject($obj);
// Then use {$obj->property} in SQL strings
```

### Singleton Services
```php
// Access services like this — never `new ServiceClass()`
WorkflowService::getInstance()->saveWorkflow($dto);
```

### Response Format
```php
// Success
return ResponseUtils::result($response, $data);

// Error
return ResponseUtils::fault($response, $exception);
```

### Validation (Respect\Validation)
```php
use Respect\Validation\Validator as v;

v::attribute("name", v::stringType()->notEmpty())
  ->attribute("type", v::in(["STAFF", "STUDENT"])->notEmpty())
  ->assert($dto);
```

### JSON Fields in DB
- Store as `longtext` or `text` in MySQL
- Use `OBJECT_FROM_JSON` in mapper to auto-decode
- When saving: `json_encode($dto->rule)` before inserting

---

## 5. What NOT to Do

- Do NOT use `new ServiceClass()` — always use `::getInstance()`
- Do NOT put business logic in controllers — delegate to services
- Do NOT skip `realEscapeObject()` before building SQL strings
- Do NOT add new global variables beyond what's already defined
- Do NOT create controllers without extending `BaseController`
- Do NOT write raw SQL in controllers — only in services
- Do NOT bypass the permission system unless `$isPermissionsRequired = false` is explicitly needed
