Multiple files in Apartment
August 19, 2025 · AI Conversations
use SilverStripeORMDataObject;
use SilverStripeSecurityMember;
class Apartment extends DataObject
{
private static $table_name = 'Apartment';
private static $db = [
'ObjectNumber' => 'Varchar',
'GroupNumber' => 'Varchar',
'WohnungType' => 'Varchar',
'Uberschrift' => 'HTMLText',
'Mietwohnung' => 'HTMLText',
'Dieumgebung' => 'HTMLText',
'BilderId' => 'Int',
'VideoId' => 'Int',
'DocumentId'=>'Int'
'GrundId' => 'Int',
'ContactId'=>'Int',
'MemberID' => 'Int'
];
private static $has_one = [
'Member' => Member::class
];
}
we need to store multiple file Bilder,video
Here’s a clean, production‑friendly setup.
—
### 1) Apartment model (owns files, sortable lists)
<?php
namespace AppModel;
use SilverStripeORMDataObject;
use SilverStripeSecurityMember;
use SilverStripeFormsFieldList;
use SilverStripeFormsGridFieldGridField;
use SilverStripeFormsGridFieldGridFieldConfig_RelationEditor;
use SilverStripeAssetsImage;
use SilverStripeAssetsFile;
use SymbioteGridFieldExtensionsGridFieldOrderableRows;
class Apartment extends DataObject
{
private static $table_name = 'Apartment';
private static $db = [
'ObjectNumber' => 'Varchar',
'GroupNumber' => 'Varchar',
'WohnungType' => 'Varchar',
'Uberschrift' => 'HTMLText',
'Mietwohnung' => 'HTMLText',
'Dieumgebung' => 'HTMLText',
'BilderId' => 'Int',
'VideoId' => 'Int',
'DocumentId' => 'Int', // <-- missing comma fixed
'GrundId' => 'Int',
'ContactId' => 'Int',
'MemberID' => 'Int'
];
private static $has_one = [
'Member' => Member::class
];
// Multiple media via relation objects
private static $has_many = [
'Bilder' => ApartmentImage::class,
'Videos' => ApartmentVideo::class,
];
// Ensure related files publish with the Apartment
private static $owns = [
'Bilder',
'Videos',
];
public function getCMSFields()
{
$fields = parent::getCMSFields();
// Images grid (sortable)
$imgConfig = GridFieldConfig_RelationEditor::create();
if (class_exists(GridFieldOrderableRows::class)) {
$imgConfig->addComponent(new GridFieldOrderableRows('Sort'));
}
$fields->addFieldToTab(
'Root.Bilder',
GridField::create('Bilder', 'Bilder (Images)', $this->Bilder(), $imgConfig)
);
// Videos grid (sortable)
$vidConfig = GridFieldConfig_RelationEditor::create();
if (class_exists(GridFieldOrderableRows::class)) {
$vidConfig->addComponent(new GridFieldOrderableRows('Sort'));
}
$fields->addFieldToTab(
'Root.Videos',
GridField::create('Videos', 'Videos', $this->Videos(), $vidConfig)
);
return $fields;
}
}
—
### 2) Relation objects with upload fields
<?php
namespace AppModel;
use SilverStripeORMDataObject;
use SilverStripeAssetsImage;
use SilverStripeAssetsFile;
use SilverStripeFormsFieldList;
use SilverStripeAssetAdminFormsUploadField;
class ApartmentImage extends DataObject
{
private static $table_name = 'ApartmentImage';
private static $db = [
'Caption' => 'Varchar(255)',
'Sort' => 'Int',
];
private static $has_one = [
'Apartment' => Apartment::class,
'Image' => Image::class,
];
private static $owns = ['Image'];
private static $summary_fields = [
'Image.CMSThumbnail' => 'Preview',
'Caption' => 'Caption',
'Sort' => 'Sort'
];
public function getCMSFields()
{
$fields = parent::getCMSFields();
$upload = UploadField::create('Image', 'Bild')
->setAllowedFileCategories('image') // images only
->setIsMultiUpload(false);
// Optional: keep images in a nice folder
$upload->setFolderName('apartments/images');
$fields->replaceField('ImageID', $upload);
return $fields;
}
}
<?php
namespace AppModel;
use SilverStripeORMDataObject;
use SilverStripeAssetsFile;
use SilverStripeFormsFieldList;
use SilverStripeAssetAdminFormsUploadField;
class ApartmentVideo extends DataObject
{
private static $table_name = 'ApartmentVideo';
private static $db = [
'Title' => 'Varchar(255)',
'Sort' => 'Int',
];
private static $has_one = [
'Apartment' => Apartment::class,
'Video' => File::class, // use File for MP4, MOV, etc.
];
private static $owns = ['Video'];
private static $summary_fields = [
'Title' => 'Title',
'Video.Filename' => 'File',
'Sort' => 'Sort'
];
public function getCMSFields()
{
$fields = parent::getCMSFields();
$upload = UploadField::create('Video', 'Video')
->setAllowedExtensions(['mp4','mov','webm','m4v','avi','mkv'])
->setIsMultiUpload(false);
$upload->setFolderName('apartments/videos');
$fields->replaceField('VideoID', $upload);
return $fields;
}
}
—
### 3) (Alternative quick setup) many_many without captions
If you *don’t* need captions/per-item fields, you can skip the relation objects:
use SilverStripeAssetsImage;
use SilverStripeAssetsFile;
private static $many_many = [
'Bilder' => Image::class,
'Videos' => File::class,
];
private static $owns = ['Bilder','Videos'];
Then use `UploadField::create('Bilder')` / `UploadField::create('Videos')` in `getCMSFields()`.
**Note:** sorting and captions are harder with plain `many_many`.
—
### 4) Template usage
<% if $Bilder %>
<% loop $Bilder.Sort('Sort') %>
<% if $Image %>
<img src="$Image.ScaleWidth(1200).URL" alt="$Caption.ATT" />
<% if $Caption %><p>$Caption</p><% end_if %>
<% end_if %>
<% end_loop %>
<% end_if %>
<% if $Videos %>
<% loop $Videos.Sort('Sort') %>
<% if $Video %>
<a href="$Video.URL">$Title</a>
<% end_if %>
<% end_loop %>
<% end_if %>
—
### 5) Install notes
– If you want drag‑drop sorting: `composer require symbiote/silverstripe-gridfield-extensions`.
– Run: `dev/build?flush=1` after adding these classes.
– Ensure filesystem permissions allow uploads.
– For publishing: since `$owns` is set, publishing the `Apartment` will also publish attached files.
This gives you clean multi‑file support for images and videos with ordering, captions, and proper publishing behavior.
In SilverStripe, the correct/clean way is to use **`has_one` to File/Image** (not raw `Int`). SilverStripe will create the `…ID` columns for you and `UploadField` will Just Work.
### Model
namespace AppModel;
use SilverStripeORMDataObject;
use SilverStripeSecurityMember;
use SilverStripeAssetsFile;
use SilverStripeAssetsImage;
class Apartment extends DataObject
{
private static $table_name = 'Apartment';
private static $db = [
'ObjectNumber' => 'Varchar',
'GroupNumber' => 'Varchar',
'WohnungType' => 'Varchar',
'Uberschrift' => 'HTMLText',
'Mietwohnung' => 'HTMLText',
'Dieumgebung' => 'HTMLText',
// REMOVE any manual Int columns like BilderId/VideoId/etc
'ContactId' => 'Int',
];
private static $has_one = [
'Member' => Member::class,
'Bilder' => Image::class, // creates BilderID (int) in DB
'Video' => File::class, // VideoID
'Document' => File::class, // DocumentID
'Grund' => File::class, // GrundID
];
// Ensures files publish with the Apartment
private static $owns = ['Bilder','Video','Document','Grund'];
}
### CMS fields (single upload each)
use SilverStripeFormsFieldList;
use SilverStripeAssetAdminFormsUploadField;
public function getCMSFields()
{
$fields = parent::getCMSFields();
$bilder = UploadField::create('Bilder', 'Bild')
->setAllowedFileCategories('image')
->setFolderName('apartments/images');
$video = UploadField::create('Video', 'Video')
->setAllowedExtensions(['mp4','mov','webm','m4v','avi','mkv'])
->setFolderName('apartments/videos');
$document = UploadField::create('Document', 'Dokument')
->setAllowedFileCategories('document')
->setFolderName('apartments/documents');
$grund = UploadField::create('Grund', 'Grundriss')
->setAllowedExtensions(['pdf','png','jpg','jpeg','webp'])
->setFolderName('apartments/grundrisse');
$fields->addFieldsToTab('Root.Main', [$bilder, $video, $document, $grund]);
return $fields;
}
### Template usage
<% if $Bilder %><img src="$Bilder.ScaleMax(1200,800).URL" alt="" /><% end_if %>
<% if $Video %><a href="$Video.URL">$Video.Name</a><% end_if %>
<% if $Document %><a href="$Document.URL">$Document.Name</a><% end_if %>
<% if $Grund %><a href="$Grund.URL">$Grund.Name</a><% end_if %>
### Notes
– Use **`Bilder` / `Video` / `Document` / `Grund`** relations; the framework creates **`BilderID`**, **`VideoID`**, etc. You **don’t** need manual `Int` fields—remove them if present.
– Run **`/dev/build?flush=1`** after changes.
– If you ever need *multiple* files per type, switch to `has_many`/`many_many`; for now you’re set with one ID per field.
Here’s how you’d set it up in your `Apartment` model + a form:
—
### Model (`Apartment.php`)
Keep the `has_one` setup to `File`/`Image`, because that automatically gives you `BilderID`, `VideoID`, `DocumentID`, `GrundID` in the DB:
private static $has_one = [
'Bilder' => SilverStripeAssetsImage::class,
'Video' => SilverStripeAssetsFile::class,
'Document' => SilverStripeAssetsFile::class,
'Grund' => SilverStripeAssetsFile::class,
];
—
### Controller form (using `FileField`)
use SilverStripeFormsForm;
use SilverStripeFormsFieldList;
use SilverStripeFormsFileField;
use SilverStripeFormsFormAction;
use SilverStripeFormsRequiredFields;
use SilverStripeAssetsUpload;
public function ApartmentForm()
{
$fields = FieldList::create(
FileField::create('Bilder', 'Bild'),
FileField::create('Video', 'Video'),
FileField::create('Document', 'Dokument'),
FileField::create('Grund', 'Grundriss')
);
$actions = FieldList::create(
FormAction::create('doSaveApartment', 'Save')
);
$validator = RequiredFields::create();
return Form::create($this, 'ApartmentForm', $fields, $actions, $validator);
}
—
### Form handler
public function doSaveApartment($data, $form)
{
$apartment = Apartment::create();
$uploader = Upload::create();
$uploader->setFolderName('apartments/uploads');
// Save Bilder (Image)
if (!empty($_FILES['Bilder']['name'])) {
$file = new SilverStripeAssetsImage();
$uploader->loadIntoFile($_FILES['Bilder'], $file);
$apartment->BilderID = $file->ID;
}
// Save Video (File)
if (!empty($_FILES['Video']['name'])) {
$file = new SilverStripeAssetsFile();
$uploader->loadIntoFile($_FILES['Video'], $file);
$apartment->VideoID = $file->ID;
}
// Save Document (File)
if (!empty($_FILES['Document']['name'])) {
$file = new SilverStripeAssetsFile();
$uploader->loadIntoFile($_FILES['Document'], $file);
$apartment->DocumentID = $file->ID;
}
// Save Grundriss (File)
if (!empty($_FILES['Grund']['name'])) {
$file = new SilverStripeAssetsFile();
$uploader->loadIntoFile($_FILES['Grund'], $file);
$apartment->GrundID = $file->ID;
}
$apartment->write();
$form->sessionMessage('Apartment saved successfully', 'good');
return $this->redirectBack();
}
—
✅ This way:
– Your database gets **BilderID, VideoID, DocumentID, GrundID** filled with file IDs.
– You’re using **`FileField`** in the form (frontend-friendly).
– Uploading is handled via **`Upload::loadIntoFile`**, which moves the file into the assets store and attaches it.
Do you want me to also show you how to **display those uploaded files** back in the template (frontend)?
### 1) Models
**Apartment.php** (add a `has_one` to the address)
namespace AppModel;
use SilverStripeORMDataObject;
use SilverStripeSecurityMember;
use SilverStripeAssetsFile;
use SilverStripeAssetsImage;
class Apartment extends DataObject
{
private static $table_name = 'Apartment';
private static $db = [
'ObjectNumber' => 'Varchar',
'GroupNumber' => 'Varchar',
'WohnungType' => 'Varchar',
'Uberschrift' => 'HTMLText',
'Mietwohnung' => 'HTMLText',
'Dieumgebung' => 'HTMLText',
'ContactId' => 'Int',
];
private static $has_one = [
'Member' => Member::class,
// Files (each stores the *_ID automatically)
'Bilder' => Image::class,
'Video' => File::class,
'Document' => File::class,
'Grund' => File::class,
// One address per apartment
'Address' => ApartmentAddress::class,
];
private static $owns = ['Bilder','Video','Document','Grund','Address'];
}
**ApartmentAddress.php**
namespace AppModel;
use SilverStripeORMDataObject;
class ApartmentAddress extends DataObject
{
private static $table_name = 'ApartmentAddress';
private static $db = [
'Street' => 'Varchar(255)',
'HouseNo' => 'Varchar(50)',
'City' => 'Varchar(100)',
'State' => 'Varchar(100)',
'Postcode' => 'Varchar(20)',
'Country' => 'Varchar(100)',
// Optional geo fields:
'Latitude' => 'Decimal(10,6)',
'Longitude' => 'Decimal(10,6)',
];
// Inverse pointer (no extra column; ties to Apartment.Address)
private static $belongs_to = [
'Apartment' => Apartment::class . '.Address',
];
private static $summary_fields = [
'FullAddress' => 'Address'
];
public function getFullAddress(): string
{
$parts = array_filter([
"{$this->Street} {$this->HouseNo}",
$this->City,
$this->State,
$this->Postcode,
$this->Country
]);
return implode(', ', $parts);
}
}
> Run `/dev/build?flush=1` after adding these.
—
### 2) Front-end form (you said you’ll use `FileField`)
Add address fields to your form and save them, then attach to the apartment:
use SilverStripeFormsForm;
use SilverStripeFormsFieldList;
use SilverStripeFormsTextField;
use SilverStripeFormsFileField;
use SilverStripeFormsFormAction;
use SilverStripeFormsRequiredFields;
use SilverStripeAssetsUpload;
use AppModelApartment;
use AppModelApartmentAddress;
public function ApartmentForm()
{
$fields = FieldList::create(
// Files
FileField::create('Bilder', 'Bild'),
FileField::create('Video', 'Video'),
FileField::create('Document', 'Dokument'),
FileField::create('Grund', 'Grundriss'),
// Address
TextField::create('Street', 'Street'),
TextField::create('HouseNo', 'House No.'),
TextField::create('City', 'City'),
TextField::create('State', 'State'),
TextField::create('Postcode', 'Postcode'),
TextField::create('Country', 'Country')
);
$actions = FieldList::create(
FormAction::create('doSaveApartment', 'Save')
);
return Form::create($this, 'ApartmentForm', $fields, $actions, RequiredFields::create());
}
public function doSaveApartment($data, $form)
{
$apartment = Apartment::create();
// Handle file uploads
$uploader = Upload::create()->setFolderName('apartments/uploads');
foreach (['Bilder' => SilverStripeAssetsImage::class,
'Video' => SilverStripeAssetsFile::class,
'Document' => SilverStripeAssetsFile::class,
'Grund' => SilverStripeAssetsFile::class] as $field => $class) {
if (!empty($_FILES[$field]['name'])) {
$file = new $class();
$uploader->loadIntoFile($_FILES[$field], $file);
$apartment->{$field . 'ID'} = $file->ID; // e.g. BilderID, VideoID...
}
}
// Create & attach address
$addr = ApartmentAddress::create([
'Street' => $data['Street'] ?? '',
'HouseNo' => $data['HouseNo'] ?? '',
'City' => $data['City'] ?? '',
'State' => $data['State'] ?? '',
'Postcode' => $data['Postcode'] ?? '',
'Country' => $data['Country'] ?? '',
]);
$addr->write();
$apartment->AddressID = $addr->ID;
$apartment->write();
$form->sessionMessage('Apartment saved with address.', 'good');
return $this->redirectBack();
}
—
### 3) Template usage
<% if $Address %>
<p>$Address.FullAddress</p>
<% end_if %>
This gives you a clean 1‑to‑1 **Apartment → Address** link, stores address in its own table, and keeps your file IDs exactly as you wanted (via `has_one` to File/Image).
### 1) Model: `ApartmentDetails.php`
namespace AppModel;
use SilverStripeORMDataObject;
class ApartmentDetails extends DataObject
{
private static $table_name = 'ApartmentDetails';
private static $db = [
'AreaSqm' => 'Decimal(10,2)', // e.g. 82.50
'Rooms' => 'Int', // e.g. 3
'Bathrooms' => 'Int',
'Floor' => 'Varchar(20)', // e.g. 2nd / EG
'HeatingType' => 'Varchar(100)',
'RentCold' => 'Decimal(12,2)', // basic rent
'RentWarm' => 'Decimal(12,2)', // incl. utilities
'Deposit' => 'Decimal(12,2)',
'AvailableFrom'=> 'Date',
'Description' => 'HTMLText',
];
// Inverse of Apartment.Has one Details
private static $belongs_to = [
'Apartment' => Apartment::class . '.Details',
];
private static $summary_fields = [
'AreaSqm' => 'm²',
'Rooms' => 'Rooms',
'RentCold' => 'Cold Rent',
'AvailableFrom.Nice' => 'Available',
];
}
### 2) Update `Apartment.php` (add relation)
private static $has_one = [
'Member' => SilverStripeSecurityMember::class,
'Bilder' => SilverStripeAssetsImage::class,
'Video' => SilverStripeAssetsFile::class,
'Document' => SilverStripeAssetsFile::class,
'Grund' => SilverStripeAssetsFile::class,
// One details row per apartment
'Details' => AppModelApartmentDetails::class,
];
private static $owns = ['Bilder','Video','Document','Grund','Details'];
> Run `/dev/build?flush=1` after adding the class & relation.
—
### 3) Front‑end form fields (you’re using `FileField`)
Add simple detail inputs alongside your existing file fields:
use SilverStripeFormsTextField;
use SilverStripeFormsNumericField;
use SilverStripeFormsTextareaField;
use SilverStripeFormsDateField;
public function ApartmentForm()
{
$fields = FieldList::create(
// existing FileFields:
FileField::create('Bilder', 'Bild'),
FileField::create('Video', 'Video'),
FileField::create('Document', 'Dokument'),
FileField::create('Grund', 'Grundriss'),
// Details:
NumericField::create('AreaSqm', 'Area (m²)'),
NumericField::create('Rooms', 'Rooms'),
NumericField::create('Bathrooms', 'Bathrooms'),
TextField::create('Floor', 'Floor'),
TextField::create('HeatingType', 'Heating Type'),
NumericField::create('RentCold', 'Cold Rent'),
NumericField::create('RentWarm', 'Warm Rent'),
NumericField::create('Deposit', 'Deposit'),
DateField::create('AvailableFrom', 'Available From')->setHTML5(true),
TextareaField::create('Description', 'Description')
);
$actions = FieldList::create(FormAction::create('doSaveApartment', 'Save'));
return Form::create($this, 'ApartmentForm', $fields, $actions);
}
### 4) Save handler (create `ApartmentDetails`, attach to `Apartment`)
Add this to your existing `doSaveApartment` handler after file‑upload logic:
use AppModelApartmentDetails;
public function doSaveApartment($data, $form)
{
$apartment = AppModelApartment::create();
// ... your existing FileField upload code that sets BilderID, VideoID, DocumentID, GrundID ...
// Create & attach details
$details = ApartmentDetails::create([
'AreaSqm' => $data['AreaSqm'] ?? null,
'Rooms' => $data['Rooms'] ?? null,
'Bathrooms' => $data['Bathrooms'] ?? null,
'Floor' => $data['Floor'] ?? '',
'HeatingType' => $data['HeatingType'] ?? '',
'RentCold' => $data['RentCold'] ?? null,
'RentWarm' => $data['RentWarm'] ?? null,
'Deposit' => $data['Deposit'] ?? null,
'AvailableFrom'=> $data['AvailableFrom']?? null,
'Description' => $data['Description'] ?? '',
]);
$details->write();
$apartment->DetailsID = $details->ID;
$apartment->write();
$form->sessionMessage('Apartment saved with details.', 'good');
return $this->redirectBack();
}
—
### 5) Template usage
<% if $Details %>
<ul class="apartment-details">
<% if $Details.AreaSqm %><li>Area: $Details.AreaSqm m²</li><% end_if %>
<% if $Details.Rooms %><li>Rooms: $Details.Rooms</li><% end_if %>
<% if $Details.Bathrooms %><li>Bathrooms: $Details.Bathrooms</li><% end_if %>
<% if $Details.Floor %><li>Floor: $Details.Floor</li><% end_if %>
<% if $Details.HeatingType %><li>Heating: $Details.HeatingType</li><% end_if %>
<% if $Details.RentCold %><li>Cold Rent: $Details.RentCold</li><% end_if %>
<% if $Details.RentWarm %><li>Warm Rent: $Details.RentWarm</li><% end_if %>
<% if $Details.Deposit %><li>Deposit: $Details.Deposit</li><% end_if %>
<% if $Details.AvailableFrom %><li>Available: $Details.AvailableFrom.Nice</li><% end_if %>
</ul>
<% if $Details.Description %><div class="desc">$Details.Description</div><% end_if %>
<% end_if %>
That’s it—**`ApartmentDetails`** is now a clean one‑to‑one companion to **`Apartment`**, saved from the same front‑end form, and publish‑linked via `$owns`.