Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
strategy:
fail-fast: false
matrix:
php: [8.2, 8.3, 8.4]
php: [8.3, 8.4]
laravel: [11.*, 12.*]
database: [mysql, pgsql]
include:
Expand Down
95 changes: 80 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,24 +247,18 @@ use AuroraWebSoftware\FlexyField\Enums\FlexyFieldType;

// Single select (dropdown)
Product::addFieldToSchema(
'electronics',
'color',
FlexyFieldType::STRING,
100,
null,
null,
['options' => ['red' => 'Red', 'blue' => 'Blue', 'green' => 'Green']]
schemaCode: 'electronics',
fieldName: 'color',
fieldType: FlexyFieldType::STRING,
fieldMetadata: ['options' => ['red' => 'Red', 'blue' => 'Blue', 'green' => 'Green']]
);

// Multi-select (checkboxes)
Product::addFieldToSchema(
'electronics',
'features',
FlexyFieldType::JSON,
100,
null,
null,
[
schemaCode: 'electronics',
fieldName: 'features',
fieldType: FlexyFieldType::JSON,
fieldMetadata: [
'options' => ['wifi', '5g', 'nfc', 'bluetooth'],
'multiple' => true
]
Expand All @@ -278,7 +272,78 @@ $phone->flexy->features = ['wifi', '5g']; // Multiple values
$phone->save();
```

### Validation
### Attribute Grouping

Organize related fields into groups for better UI presentation:

```php
use AuroraWebSoftware\FlexyField\Models\FieldSchema;

// Define fields with groups
Product::addFieldToSchema(
schemaCode: 'electronics',
fieldName: 'voltage',
fieldType: FlexyFieldType::STRING,
fieldMetadata: ['group' => 'Power Specs']
);

Product::addFieldToSchema(
schemaCode: 'electronics',
fieldName: 'weight_kg',
fieldType: FlexyFieldType::DECIMAL,
fieldMetadata: ['group' => 'Physical Dimensions']
);

// Fields without group metadata are ungrouped
Product::addFieldToSchema(
schemaCode: 'electronics',
fieldName: 'name',
fieldType: FlexyFieldType::STRING
);

// Retrieve fields organized by group
$schema = FieldSchema::where('schema_code', 'electronics')->first();
$grouped = $schema->getFieldsGrouped();

// Iterate through groups
foreach ($grouped as $groupName => $fields) {
echo "Group: $groupName\n";
foreach ($fields as $field) {
echo " - {$field->name}\n";
}
}
```

### UI Hints

Improve UX with human-readable labels, placeholders, and hints:

```php
use AuroraWebSoftware\FlexyField\Models\SchemaField;

// Define field with UI hints
Product::addFieldToSchema(
schemaCode: 'electronics',
fieldName: 'battery_capacity_mah',
fieldType: FlexyFieldType::INTEGER,
label: 'Battery Capacity',
fieldMetadata: [
'placeholder' => 'Enter value in mAh',
'hint' => 'Typical range: 1000-5000mAh'
]
);

// Retrieve UI hints
$field = SchemaField::where('name', 'battery_capacity_mah')->first();
echo $field->getLabel(); // "Battery Capacity"
echo $field->getPlaceholder(); // "Enter value in mAh"
echo $field->getHint(); // "Typical range: 1000-5000mAh"

// Label falls back to name if null
$field->label = null;
echo $field->getLabel(); // "battery_capacity_mah"
```


```php
try {
Expand Down
10 changes: 5 additions & 5 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,16 +67,16 @@
"@php -r \"echo '\\n=== Testing with PostgreSQL ===\\n';\"",
"@composer run test:pgsql"
],
"test:mysql": "vendor/bin/pest --configuration=phpunit.xml.dist --exclude-filter=FieldSetPerformanceTest",
"test:pgsql": "vendor/bin/pest --configuration=phpunit-postgress.xml.dist --exclude-filter=FieldSetPerformanceTest",
"test:mysql": "vendor/bin/pest --configuration=phpunit.xml.dist --exclude-group=performance",
"test:pgsql": "vendor/bin/pest --configuration=phpunit-postgress.xml.dist --exclude-group=performance",
"test:performance": [
"@php -r \"echo '\\n=== Performance Tests with MySQL ===\\n';\"",
"@composer run test:performance:mysql",
"@php -r \"echo '\\n=== Performance Tests with PostgreSQL ===\\n';\"",
"@composer run test:performance:pgsql"
],
"test:performance:mysql": "vendor/bin/pest --configuration=phpunit.xml.dist tests/Feature/FieldSetPerformanceTest.php",
"test:performance:pgsql": "vendor/bin/pest --configuration=phpunit-postgress.xml.dist tests/Feature/FieldSetPerformanceTest.php",
"test:performance:mysql": "vendor/bin/pest --configuration=phpunit.xml.dist --group=performance",
"test:performance:pgsql": "vendor/bin/pest --configuration=phpunit-postgress.xml.dist --group=performance",
"test-coverage": "vendor/bin/pest --coverage",
"format": "vendor/bin/pint"
},
Expand All @@ -99,4 +99,4 @@
},
"minimum-stability": "dev",
"prefer-stable": true
}
}
26 changes: 26 additions & 0 deletions database/migrations/add_label_column_to_schema_fields.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
public function up(): void
{
Schema::table('ff_schema_fields', function (Blueprint $table) {
if (! Schema::hasColumn('ff_schema_fields', 'label')) {
$table->string('label')->nullable()->after('name');
}
});
}

public function down(): void
{
Schema::table('ff_schema_fields', function (Blueprint $table) {
if (Schema::hasColumn('ff_schema_fields', 'label')) {
$table->dropColumn('label');
}
});
}
};
83 changes: 75 additions & 8 deletions docs/BEST_PRACTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,15 +82,23 @@ public function rules() {

```php
// ✅ Correct
Product::addFieldToSchema('product', 'tags', FlexyFieldType::JSON, 100, null, null, [
'options' => ['new', 'sale', 'featured'],
'multiple' => true
]);
Product::addFieldToSchema(
schemaCode: 'product',
fieldName: 'tags',
fieldType: FlexyFieldType::JSON,
fieldMetadata: [
'options' => ['new', 'sale', 'featured'],
'multiple' => true
]
);

// ❌ Wrong - STRING type with multiple: true will fail
Product::addFieldToSchema('product', 'tags', FlexyFieldType::STRING, 100, null, null, [
'multiple' => true // Error: multi-select needs JSON type
]);
Product::addFieldToSchema(
schemaCode: 'product',
fieldName: 'tags',
fieldType: FlexyFieldType::STRING,
fieldMetadata: ['multiple' => true] // Error: multi-select needs JSON type
);
```

**Keep options manageable:**
Expand All @@ -99,7 +107,66 @@ Product::addFieldToSchema('product', 'tags', FlexyFieldType::STRING, 100, null,
- **Medium lists** (10-50 items): Store in config/constants
- **Large/dynamic lists** (> 50 items): Use relationships instead of select options

## Data Migration
## Attribute Grouping

**Organize fields by functional area:**

```php
// Group related fields together
['group' => 'Technical Specs'] // voltage, power_consumption, etc.
['group' => 'Physical'] // weight, dimensions, etc.
['group' => 'Marketing'] // description, features, etc.
```

**Naming conventions:**

- **Title Case:** "Power Specs", "Physical Dimensions"
- **Keep short:** Max ~30 characters for UI readability
- **Be consistent:** Use same group names across similar schemas

**UI Recommendations:**

- Display groups alphabetically (case-insensitive)
- Show "Ungrouped" fields last
- Use collapsible sections for each group
- Consider icons/colors for visual distinction

## UI Hints

**Use descriptive labels:**

```php
// ✅ Good
label: 'Battery Capacity'
label: 'Product Weight'

// ❌ Bad
label: 'bat_cap' // Too technical
label: null // Missing user-friendly name
```

**Write helpful placeholders:**

```php
// ✅ Specific format examples
'placeholder' => 'e.g., 99.99'
'placeholder' => 'Enter email address'

// ❌ Vague
'placeholder' => 'Enter value'
```

**Provide actionable hints:**

```php
// ✅ Clear constraints
'hint' => 'Must be between 1000-5000mAh'
'hint' => 'Leave empty for auto-generation'

// ❌ Redundant
'hint' => 'Enter battery capacity' // Already in label
```


**Add fields** (no migration needed):

Expand Down
17 changes: 0 additions & 17 deletions openspec/changes/add-attribute-grouping-support/proposal.md

This file was deleted.

53 changes: 53 additions & 0 deletions openspec/changes/add-simple-localization-support/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Design: Simple Localization Support

## Goal
Enable storing and retrieving localized content (e.g., product descriptions in multiple languages) within a single field, without creating separate fields for each language.

## Technical Approach

### 1. Storage Strategy
We will use the existing `FlexyFieldType::JSON` type to store localized data.
- **Database:** Stored in `value_json` column in `ff_field_values`.
- **Format:** `{"en": "Blue", "tr": "Mavi", "de": "Blau"}`

### 2. Metadata Configuration
A new flag `is_translatable` in the `metadata` JSON column of `ff_schema_fields` will indicate if a field is localized.

```json
{
"is_translatable": true
}
```

### 3. Accessor Logic (The "Magic")
We need to intercept access to the field value in `src/Traits/Flexy.php` (specifically `__get`).

- **Current Behavior:** Returns the raw value (string or array).
- **New Behavior:**
- If field is `is_translatable`:
- `__get('desc')`: Returns value for current app locale (`app()->getLocale()`).
- If current locale missing, fallback to fallback locale.
- If both missing, return `null` or empty string.

### 4. Explicit Retrieval
We need a way to get the raw translations or a specific locale.
- `$model->flexy->getTranslation('desc', 'tr')`
- `$model->flexy->getTranslations('desc')` // Returns full array

### 5. Setting Values
- **Direct Assignment:** `$model->flexy->desc = 'Blue'` -> Sets value for *current locale*, merging with existing JSON.
- **Bulk Assignment:** `$model->flexy->desc = ['en' => 'Blue', 'tr' => 'Mavi']` -> Replaces/Merges based on array input.

### 6. Validation
Validation needs to be locale-aware.
- `required`: At least one locale (or default locale) must have a value.
- `string`, `min`, `max`: Applied to the value of the *current locale* or all locales?
- *Decision:* Apply rules to the *values* within the JSON.

## Constraints
- Translatable fields **MUST** be `FlexyFieldType::JSON`.
- Keys in JSON must be valid locale strings (2-5 chars).

## Architecture
- **SchemaField Model:** Add `isTranslatable()` helper.
- **Flexy Trait:** Update `__get` and `__set` logic to handle translation merging and retrieval.
40 changes: 40 additions & 0 deletions openspec/changes/add-simple-localization-support/proposal.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,50 @@ PIM and CMS systems require storing content in multiple languages (e.g., Product
- `$model->flexy->getTranslation('desc', 'en')` for specific locale.
- **Validation:** Update validation logic to validate values inside the JSON structure (e.g., `required` means at least default locale must be present).

## Usage Example

```php
use AuroraWebSoftware\FlexyField\Enums\FlexyFieldType;

// Define translatable field
Product::addFieldToSchema(
schemaCode: 'product',
fieldName: 'description',
fieldType: FlexyFieldType::JSON, // Must be JSON
fieldMetadata: ['is_translatable' => true]
);

// Usage
app()->setLocale('en');
$product->flexy->description = 'Blue Shirt'; // Sets {"en": "Blue Shirt"}

app()->setLocale('tr');
$product->flexy->description = 'Mavi Gömlek'; // Merges: {"en": "Blue Shirt", "tr": "Mavi Gömlek"}

// Retrieval
echo $product->flexy->description; // Output: "Mavi Gömlek" (current locale)
echo $product->flexy->getTranslation('description', 'en'); // Output: "Blue Shirt"
```

## Constraints
- **Field Type:** Must be `FlexyFieldType::JSON`.
- **Storage:** Stored as JSON object `{"locale": "value"}`.
- **Validation:** `required` rule checks if the JSON is not empty.

## Impact
- **Affected specs:** `type-system`, `field-validation`
- **Affected code:**
- `src/Models/SchemaField.php`: Add `isTranslatable()` helper.
- `src/Traits/Flexy.php`: Update accessor logic to handle translatable JSON fields.
- **Breaking changes:** None.
- **Database changes:** None (uses existing `JSON` type and `metadata`).

## Requirements
- **Clean Architecture:** Logic should be encapsulated in Traits or a dedicated Value Object if complex.
- **Testing:**
- Comprehensive feature tests in `tests/Feature/SimpleLocalizationTest.php`.
- Test locale switching and fallback.
- **Quality:**
- Must pass `phpstan` (Larastan) level 5+.
- Must pass `pint` style checks.

Loading