Remove public from URL
August 18, 2025 · AI Conversations
/public
# Recommended (best practice): point your webroot to `/public`
**Apache (vhost)**
<VirtualHost *:80>
ServerName example.com
DocumentRoot /var/www/site/current/public
<Directory /var/www/site/current/public>
AllowOverride All
Require all granted
</Directory>
</VirtualHost>
**Nginx**
server {
server_name example.com;
root /var/www/site/current/public;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ .php$ {
include fastcgi_params;
fastcgi_pass unix:/run/php/php8.2-fpm.sock; # adjust
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
}
}
**cPanel / Plesk**
– Change the **Document Root** of the domain to `/public` (Domains → Document Root).
No URL will show `/public` after this.
—
# If you **can’t** change the document root (shared hosting)
Add this **.htaccess in your project root (above `/public`)** so Apache internally rewrites to `/public` without showing it:
<IfModule mod_rewrite.c>
RewriteEngine On
# Don’t loop if the request is already for /public
RewriteRule ^public/ - [L]
# Serve real files/folders that exist in the root (e.g. .well-known)
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]
# Internally route everything else to /public
RewriteRule ^(.*)$ public/$1 [L]
</IfModule>
This keeps the browser at `https://example.com/…` while the server serves from `/public`.
—
# Silverstripe config tips
– In `.env`, set your base URL to the root domain (no `/public`):
SS_BASE_URL="https://example.com"
– After changing server config, hit your site with `?flush` (e.g., `https://example.com/?flush`) to refresh cached configs/routes.
—
# What **not** to do
– Don’t move everything out of `/public` or change Composer’s `public-dir` to `.` unless you absolutely must; it weakens security. Using `/public` as the webroot is the intended, safer setup.
If you tell me your stack (Apache/Nginx/cPanel) and where your project lives, I can drop in the exact path-ready snippet.
# 1) Add an Image relation on your object
Example `DataObject` (works the same on a `Page` subclass).
namespace AppModel;
use SilverStripeORMDataObject;
use SilverStripeAssetsImage;
class Profile extends DataObject
{
private static $table_name = 'Profile';
private static $db = [
'Title' => 'Varchar(255)',
];
private static $has_one = [
'Photo' => Image::class, // <-- the uploaded image
];
// So the image auto-publishes with the record
private static $owns = [
'Photo',
];
private static $summary_fields = [
'Title',
'Photo.CMSThumbnail' => 'Photo',
];
}
Run `/dev/build?flush`.
—
# 2) Upload via CMS (ModelAdmin/Page)
In your `DataObject` or `Page`:
use SilverStripeFormsFieldList;
use SilverStripeFormsTextField;
use SilverStripeAssetAdminFormsUploadField;
public function getCMSFields()
{
$fields = parent::getCMSFields();
$fields->addFieldsToTab('Root.Main', [
TextField::create('Title'),
UploadField::create('Photo', 'Profile Photo')
->setFolderName('profiles') // assets/profiles/
->setAllowedExtensions(['jpg','jpeg','png','webp'])
->setAllowedMaxFileNumber(1)
->setIsMultiUpload(false),
]);
return $fields;
}
Publish the record. Because of `$owns = ['Photo']`, the image will also go Live.
—
# 3) Upload from a **front-end form** (controller)
## Form
use SilverStripeFormsForm;
use SilverStripeFormsFieldList;
use SilverStripeFormsTextField;
use SilverStripeFormsFormAction;
use SilverStripeFormsRequiredFields;
use SilverStripeAssetAdminFormsUploadField;
use SilverStripeControlController;
use AppModelProfile;
class ProfileController extends Controller
{
private static $allowed_actions = ['Form','save'];
public function Form()
{
$fields = FieldList::create(
TextField::create('Title'),
UploadField::create('Photo', 'Upload photo')
->setFolderName('profiles')
->setAllowedExtensions(['jpg','jpeg','png','webp'])
->setIsMultiUpload(false)
);
$actions = FieldList::create(
FormAction::create('save', 'Save')
);
$validator = RequiredFields::create(['Title', 'Photo']);
return Form::create($this, 'Form', $fields, $actions, $validator);
}
public function save($data, Form $form)
{
$record = Profile::create();
// This will pull the uploaded file from UploadField and set PhotoID
$form->saveInto($record);
$record->write();
// Make sure the image is publicly visible
if ($record->PhotoID && $record->Photo()->isDraft()) {
$record->Photo()->publishSingle();
}
return $this->redirect($this->Link('?saved=1'));
}
}
> Note: Front-end uploads require permission to create files. If your users aren’t CMS users, gate the action (e.g., only for logged-in members) or add a custom policy/extension to allow uploads safely.
—
# 4) Show the image in a template
In your `.ss` template (e.g., `ProfilePage.ss` or where you loop items):
<% if $Photo %>
<img src="$Photo.Fill(800,500).URL" alt="$Title.ATT">
<% else %>
<p>No photo uploaded.</p>
<% end_if %>
Handy variants:
– `$Photo.ScaleMax(1200, 800).URL`
– `$Photo.Fit(600, 400).URL`
– `$Photo.URL` (original)
—
# 5) Common gotchas
– **Don’t move `/public`**: keep webroot at `/public` for security.
– **Publishing**: If content must be visible to anonymous users, ensure the image is **published**. Either call `$image->publishSingle()` (as above) or rely on `$owns` and publish the parent record.
– **Flush**: After code changes, open any URL with `?flush` to clear caches.
– **Folder names**: `setFolderName('profiles')` stores files under `assets/profiles/`.
If you share which object you’re attaching the image to (e.g., `MemberBasicData`, `RentalWorkerInformation`, or a `Page`), I’ll tailor the exact code block for that class.
->setAllowedExtensions(['jpg', 'jpeg', 'png'])
->setAttribute('id', 'fileUpload')
->setAttribute('accept', 'image/jpeg, image/png')
->setAttribute('style', 'display:none;'),
I've created field like this
## 1) DataObject relation
use SilverStripeORMDataObject;
use SilverStripeAssetsImage;
class Profile extends DataObject
{
private static $db = ['Title' => 'Varchar(255)'];
private static $has_one = [
'Bilder' => Image::class, // matches your UploadField name
];
private static $owns = ['Bilder']; // auto-publish with record
}
Run `/dev/build?flush`.
## 2) Field (keep yours, add single-file + folder)
use SilverStripeAssetAdminFormsUploadField;
$upload = UploadField::create('Bilder', 'Upload Image')
->setAllowedExtensions(['jpg','jpeg','png'])
->setIsMultiUpload(false)
->setAllowedMaxFileNumber(1)
->setFolderName('profiles')
->setAttribute('id', 'fileUpload') // wrapper id
->setAttribute('accept', 'image/jpeg, image/png')
->setAttribute('style', 'display:none;'); // you’re hiding the UI
## 3) Save (front-end controller action)
public function save($data, SilverStripeFormsForm $form)
{
$record = Profile::create();
$form->saveInto($record); // sets BilderID
$record->write();
if ($record->BilderID) {
$img = $record->Bilder();
if ($img && $img->exists() && $img->isDraft()) {
$img->publishSingle();
}
}
return $this->redirect($this->Link('?saved=1'));
}
## 4) Custom trigger button + live preview (template)
<button type="button" id="pickImage" class="button">Choose image</button>
<div id="imgPreview">
<% if $Bilder %>
<img src="$Bilder.Fit(500,350).URL" alt="$Title.ATT">
<% end_if %>
</div>
$Form <!-- make sure your form renders the hidden UploadField -->
<script>
(function(){
const trigger = document.getElementById('pickImage');
const holder = document.getElementById('fileUpload'); // UploadField wrapper
if (!trigger || !holder) return;
// Find the real <input type="file"> inside UploadField
const fileInput = holder.querySelector('input[type="file"]');
trigger.addEventListener('click', function(e){
e.preventDefault();
if (fileInput) fileInput.click();
});
// Live preview (front-end only)
holder.addEventListener('change', function(e){
if (e.target === fileInput && fileInput.files && fileInput.files[0]) {
const reader = new FileReader();
reader.onload = function(ev){
let img = document.querySelector('#imgPreview img');
if (!img) {
img = document.createElement('img');
document.getElementById('imgPreview').appendChild(img);
}
img.src = ev.target.result;
img.style.maxWidth = '100%';
img.style.display = '';
};
reader.readAsDataURL(fileInput.files[0]);
}
});
})();
</script>
## 5) Show saved image anywhere
<% if $Bilder %>
<img src="$Bilder.Fill(800,500).URL" alt="$Title.ATT">
<% end_if %>
### Notes
– Keep the field name **exactly** `Bilder` to match the `has_one` (`BilderID` is saved automatically by `saveInto`).
– `setIsMultiUpload(false)` + `setAllowedMaxFileNumber(1)` ensures single image.
– If files must be visible to guests, **publish** the image (done above).
$Title
Bilder
JPEG, PNG
$Bilder
<script>
const fileInput = document.getElementById('fileUpload');
const fileName = document.getElementById('file-name');
fileInput.addEventListener('change', function () {
if (this.files.length > 0) {
fileName.textContent = this.files[0].name;
} else {
fileName.textContent = 'Bilder';
}
});
</script>
$Description
$Form.Actions
Here’s a drop-in fix:
## 1) PHP (field setup)
Keep your field, but ensure single file + folder (optional):
$upload = UploadField::create('Bilder', 'Upload Image')
->setAllowedExtensions(['jpg','jpeg','png'])
->setIsMultiUpload(false)
->setAllowedMaxFileNumber(1)
->setFolderName('profiles')
->setAttribute('id', 'fileUpload') // id on wrapper div
->setAttribute('accept', 'image/jpeg, image/png')
->setAttribute('style', 'display:none;'); // hide UploadField UI
## 2) Template (.ss)
Use the label as your custom button, render the field, and wire the **real** file input inside the UploadField:
<!-- Your label UI -->
<label class="upload-box" for="fileUpload">
<div class="upload-left">
<img src="https://img.icons8.com/ios-filled/50/image--v1.png" alt="icon">
<span class="file-name" id="file-name" style="display:block;">Bilder</span>
</div>
<span class="file-types">JPEG, PNG</span>
</label>
<!-- Render the UploadField (hidden wrapper with id="fileUpload") -->
$Form.FieldByName(Bilder)
### JS (forward label click to the hidden input + show file name)
<script>
(function(){
// UploadField wrapper div (you set this id in PHP)
const holder = document.getElementById('fileUpload');
const label = document.querySelector('label[for="fileUpload"]');
const fileName = document.getElementById('file-name');
if (!holder || !label) return;
// UploadField uses Dropzone; the real <input type="file"> lives inside the wrapper
const getInput = () => holder.querySelector('input[type="file"]'); // usually .dz-hidden-input
// Click label => open native picker
label.addEventListener('click', function(e){
e.preventDefault();
const input = getInput();
if (input) input.click();
});
// Update filename when a file is chosen
function bindChangeOnce(input){
if (input && !input.__wired) {
input.addEventListener('change', function(e){
const f = e.target.files && e.target.files[0];
fileName.textContent = f ? f.name : 'Bilder';
});
input.__wired = true;
}
}
// Input can be created dynamically; observe and bind when it appears
const observer = new MutationObserver(() => bindChangeOnce(getInput()));
observer.observe(holder, { childList: true, subtree: true });
// If it already exists, bind now
bindChangeOnce(getInput());
})();
</script>
> Why this works: `for="fileUpload"` targets your hidden **wrapper**; we intercept that click and programmatically click the real file input inside UploadField. We also watch the wrapper for dynamic changes (Dropzone adds the input), then bind a `change` listener to show the chosen filename.
## 3) Showing the saved image later
<% if $Bilder %>
<img src="$Bilder.Fit(800,600).URL" alt="$Title.ATT">
<% end_if %>
## 4) Ensure it actually saves & publishes (controller)
public function save($data, SilverStripeFormsForm $form)
{
$record = Profile::create();
$form->saveInto($record); // sets BilderID from UploadField
$record->write();
if ($record->BilderID) {
$img = $record->Bilder();
if ($img && $img->exists() && $img->isDraft()) {
$img->publishSingle();
}
}
return $this->redirect($this->Link('?saved=1'));
}
If `$Form.FieldByName(Bilder)` doesn’t render in your template, use `$Form.Fields.dataFieldByName(Bilder)` instead.
$form = $this->Step5Form();
$sessionData = $this->getRequest()->getSession()->get('MBData');
if($sessionData['InseriereAls'] == 'broker'){
$title = 'Firma omschrijving';
}else{
$title = 'Personaliche Angaben';
}
return $this->customise([
'Form' => $form,
'Title' => $title,
'Bilder' => $form->Fields()->dataFieldByName('Bilder'),
'Description' => $form->Fields()->dataFieldByName('Description')
])->renderWith(['Layout/RegistrationStep5', 'Page']);
}
above is the function that showing option for upload image
## 1) Make sure the UploadField has the wrapper id
In your `Step5Form()` (or wherever you build the form), set the id on the **UploadField wrapper** so your label/JS can target it:
UploadField::create('Bilder', 'Upload Image')
->setFolderName('profiles')
->setAllowedExtensions(['jpg','jpeg','png'])
->setIsMultiUpload(false)
->setAllowedMaxFileNumber(1)
->setAttribute('id', 'fileUpload'); // <-- wrapper div id
## 2) Render the form safely (CSRF + enctype) and wire the real input
Instead of a manual “, use the built-ins so Silverstripe adds `multipart/form-data` and the security token automatically:
$Form.OpenTag
<!-- Your custom label/button -->
<label class="upload-box" for="fileUpload">
<div class="upload-left">
<img src="https://img.icons8.com/ios-filled/50/image--v1.png" alt="icon">
<span class="file-name" id="file-name" style="display:block;">Bilder</span>
</div>
<span class="file-types">JPEG, PNG</span>
</label>
<!-- Hidden UploadField wrapper -->
$Bilder
<!-- Other fields -->
$Description
$Form.Actions
$Form.CloseTag
If you want to keep your manual “, then **also include** the token:
$Form.SecurityToken
## 3) JS: click-through + filename (target the real “ inside UploadField)
<script>
(function(){
const holder = document.getElementById('fileUpload'); // UploadField wrapper div
const label = document.querySelector('label[for="fileUpload"]');
const fileName = document.getElementById('file-name');
if (!holder || !label) return;
// Real file input lives inside the wrapper (Dropzone creates it)
const getInput = () => holder.querySelector('input[type="file"]');
label.addEventListener('click', function(e){
e.preventDefault();
const input = getInput();
if (input) input.click();
});
function bindChangeOnce(input){
if (input && !input.__wired) {
input.addEventListener('change', function(e){
const f = e.target.files && e.target.files[0];
fileName.textContent = f ? f.name : 'Bilder';
});
input.__wired = true;
}
}
// Input can be added later; observe then bind
const obs = new MutationObserver(() => bindChangeOnce(getInput()));
obs.observe(holder, { childList: true, subtree: true });
// Bind immediately if already present
bindChangeOnce(getInput());
})();
</script>
## 4) Save & publish on submit (controller action)
public function doStep5($data, SilverStripeFormsForm $form)
{
$record = YourDataObject::create();
$form->saveInto($record); // sets BilderID + Description
$record->write();
if ($record->BilderID && $record->Bilder()->isDraft()) {
$record->Bilder()->publishSingle();
}
return $this->redirect($this->Link('step5') . '?saved=1');
}
That’s it—your label will open the native picker, the filename shows up, the form posts with the right `enctype` and CSRF, and the image gets saved + published.
$data = $request->postVars();
$sessionData = $this->getRequest()->getSession()->get('MBData');
if($sessionData['InseriereAls'] == 'broker'){
$session = $this->getRequest()->getSession();
$allData = array_merge(
$session->get('BrokerCompanyData') ?: [],
$data
);
$hash = $request->getSession()->get('HashValue');
$memberID = $this->getMemberIDByField('TempIDHash', $hash);
if ($memberID) {
$companyData = MemberCompanyData::get()->filter('MemberID', $memberID)->first();
}
if (empty($companyData) || !$companyData->exists()) {
$companyData = new MemberCompanyData();
$companyData->MemberID = $memberID;
}
foreach ($allData as $key => $value) {
if ($companyData->hasField($key)) {
$companyData->$key = $value;
}
}
if (!empty($data['Bilder'])) {
$companyData->ImageID = $data['Bilder'];
}
$companyData->write();
$session->clear('BrokerCompanyData');
return $this->redirect('/public/registration/step6');
}else{
$session = $this->getRequest()->getSession();
$allData = array_merge(
$session->get('PersonalInfo') ?: [],
$data
);
$hash = $request->getSession()->get('HashValue');
$memberID = $this->getMemberIDByField('TempIDHash', $hash);
if ($memberID) {
$personalInfo = PersonalInformation::get()->filter('MemberID', $memberID)->first();
}
if (empty($personalInfo) || !$personalInfo->exists()) {
$personalInfo = new PersonalInformation();
$personalInfo->MemberID = $memberID;
}
//var_dump($data);
if (!empty($data['Bilder'])) {
$personalInfo->ImageID = $data['Bilder'];
}
foreach ($allData as $key => $value) {
if ($personalInfo->hasField($key)) {
$personalInfo->$key = $value;
}
}
$personalInfo->write();
$session->clear('PersonalInfo');
// Login script start //
$authenticator = new MemberAuthenticator();
$userName = $request->getSession()->get('userName');
$userPsd = $request->getSession()->get('userPsd');
$member = $authenticator->authenticate([
'Email' => $userName,
'Password' => $userPsd
], $request);
if ($member && $member->exists()) {
$memberBasicData = MemberBasicData::get()->filter('MemberID', $member->ID)->first();
if ($memberBasicData) {
$InseriereAls = $memberBasicData->InseriereAls;
$firstName = $memberBasicData->FirstName;
$lastName = $memberBasicData->LastName;
$request->getSession()->set('UserType', $InseriereAls);
$request->getSession()->set('CUFirstName', $firstName);
$request->getSession()->set('CULastName', $lastName);
$identityStore = Injector::inst()->get(IdentityStore::class);
$identityStore->logIn($member, false, $request);
}
}
// Login script end //
return $this->redirect('/public/registration/thankyou');
}
}
Your `DataObject` uses `ImageID`, but your form field is named `Bilder`. With Silverstripe, the UploadField’s **name must match the has_one** to let `$form->saveInto()` wire it up.
## Fix (recommended)
1) In `Step5Form()` change:
UploadField::create('Image', 'Bilder') // name = Image, label = Bilder
->setAllowedExtensions(['jpg','jpeg','png'])
->setIsMultiUpload(false)
->setAllowedMaxFileNumber(1)
->setFolderName('profiles')
->setAttribute('id', 'fileUpload')
->setAttribute('accept', 'image/jpeg, image/png')
->setAttribute('style', 'display:none;');
2) In the template, render the field by its **name**:
$Form.FieldByName(Image)
(Keep your custom label/JS as-is; it still targets `#fileUpload`.)
3) Update `doStep5()` to use the form to save and **publish** the image. No need to manually read `$data['Bilder']` anymore.
use SilverStripeFormsForm;
public function doStep5(SilverStripeControlHTTPRequest $request)
{
$data = $request->postVars();
$form = $this->Step5Form();
$form->loadDataFrom($data); // make sure validators/transformers run
$session = $this->getRequest()->getSession();
$sessionData = $session->get('MBData');
$hash = $session->get('HashValue');
$memberID = $this->getMemberIDByField('TempIDHash', $hash);
if ($sessionData['InseriereAls'] === 'broker') {
$companyData = MemberCompanyData::get()->filter('MemberID', $memberID)->first() ?: MemberCompanyData::create(['MemberID' => $memberID]);
// Save all posted fields (incl. Image) onto the object
$form->saveInto($companyData);
// If you still need to merge session fields:
$allData = array_merge($session->get('BrokerCompanyData') ?: [], $data);
foreach ($allData as $key => $value) {
if ($companyData->hasField($key)) {
$companyData->$key = $value;
}
}
$companyData->write();
// Publish the file so it’s visible to guests
if ($companyData->ImageID && $companyData->Image()->isDraft()) {
$companyData->Image()->publishSingle();
}
$session->clear('BrokerCompanyData');
return $this->redirect('/public/registration/step6');
} else {
$personalInfo = PersonalInformation::get()->filter('MemberID', $memberID)->first() ?: PersonalInformation::create(['MemberID' => $memberID]);
$form->saveInto($personalInfo);
$allData = array_merge($session->get('PersonalInfo') ?: [], $data);
foreach ($allData as $key => $value) {
if ($personalInfo->hasField($key)) {
$personalInfo->$key = $value;
}
}
$personalInfo->write();
if ($personalInfo->ImageID && $personalInfo->Image()->isDraft()) {
$personalInfo->Image()->publishSingle();
}
// (Your login script unchanged)
$authenticator = new MemberAuthenticator();
$member = $authenticator->authenticate([
'Email' => $session->get('userName'),
'Password' => $session->get('userPsd')
], $request);
if ($member && $member->exists()) {
$memberBasicData = MemberBasicData::get()->filter('MemberID', $member->ID)->first();
if ($memberBasicData) {
$identityStore = Injector::inst()->get(IdentityStore::class);
$identityStore->logIn($member, false, $request);
$session->set('UserType', $memberBasicData->InseriereAls);
$session->set('CUFirstName', $memberBasicData->FirstName);
$session->set('CULastName', $memberBasicData->LastName);
}
}
return $this->redirect('/public/registration/thankyou');
}
}
## Bonus (optional, but helpful)
– In both `MemberCompanyData` and `PersonalInformation` models:
private static $has_one = [
'Image' => SilverStripeAssetsImage::class,
];
private static $owns = ['Image']; // auto-publish with parent (if versioned)
– After any code changes, do `?flush`.
—
### If you must keep the field name `Bilder`
Then map it manually and publish:
$imageID = (int)($data['Bilder'] ?? 0); // might need to handle array shapes too
if ($imageID) {
if ($file = SilverStripeAssetsImage::get()->byID($imageID)) {
if ($file->isDraft()) $file->publishSingle();
$companyData->ImageID = $file->ID; // or $personalInfo->ImageID
}
}
…but the clean way is to **rename the UploadField to `Image`** so `$form->saveInto()` just works.
## What to change (minimal)
1) Add a tiny helper to extract the uploaded file ID from UploadField (it can post as an int or array):
use SilverStripeAssetsImage;
private function extractUploadID($value): int
{
if (!$value) return 0;
if (is_numeric($value)) return (int)$value;
if (is_array($value)) {
if (isset($value['Files']) && is_array($value['Files']) && !empty($value['Files'])) {
return (int)$value['Files'][0];
}
if (isset($value['id'])) return (int)$value['id'];
if (isset($value[0])) return (int)$value[0];
}
return 0;
}
2) In your `doStep5()` keep `Bilder` as the field name, but map it to `ImageID` and publish:
### Broker branch
$imgID = $this->extractUploadID($data['Bilder'] ?? null);
if ($imgID) {
if ($file = Image::get()->byID($imgID)) {
$file->publishSingle(); // make it visible to guests
$companyData->ImageID = $file->ID; // map Bilder -> ImageID
}
}
### Personal branch
$imgID = $this->extractUploadID($data['Bilder'] ?? null);
if ($imgID) {
if ($file = Image::get()->byID($imgID)) {
$file->publishSingle();
$personalInfo->ImageID = $file->ID;
}
}
> Keep the rest of your merging and `write()` calls as-is.
That’s it—**field shown in the form is `Bilder`**, your template keeps using `$Form.FieldByName(Bilder)`, and the saved relation remains `ImageID` on your objects.
—
### Optional (DB-level German naming)
If you want the **DB relation** to also be German, rename your `has_one` to:
private static $has_one = [
'Bilder' => Image::class,
];
private static $owns = ['Bilder'];
…then `saveInto()` will work automatically with `UploadField::create('Bilder', …)`.
But this requires a data migration from `ImageID` → `BilderID` and a `/dev/build?flush`. If you don’t want schema changes, stick to the simple mapping above.
$record->Bilder()->publishSingle();
}
want to save like this
So tell How I will show image at frontend
if ($record->BilderID && $record->Bilder()->isDraft()) {
$record->Bilder()->publishSingle();
}
you can show it on the frontend like this.
# In a template for that object
<% if $Bilder %>
<img src="$Bilder.Fit(800,600).URL" alt="$Title.ATT">
<% else %>
<p>No image uploaded.</p>
<% end_if %>
Common variants:
$Bilder.URL <!-- original -->
$Bilder.Fill(600,400).URL <!-- crop to exact size -->
$Bilder.Fit(600,400).URL <!-- fit inside box -->
$Bilder.ScaleMax(1200,800).URL <!-- keep aspect ratio, max bounds -->
Optional responsive example:
<img
src="$Bilder.ScaleMax(600,400).URL"
srcset="$Bilder.ScaleMax(600,400).URL 600w,
$Bilder.ScaleMax(900,600).URL 900w,
$Bilder.ScaleMax(1200,800).URL 1200w"
sizes="(max-width: 700px) 100vw, 700px"
alt="$Title.ATT">
# If you’re passing a record to the view
Example (Step 6 / Thank You page):
public function step6()
{
$memberID = Security::getCurrentUser()?->ID;
$company = MemberCompanyData::get()->filter('MemberID', $memberID)->first();
return $this->customise(['Company' => $company])
->renderWith(['Layout/RegistrationStep6','Page']);
}
Template:
<% if $Company && $Company.Bilder %>
<img src="$Company.Bilder.Fit(800,600).URL" alt="$Company.Title.ATT">
<% end_if %>
# Model tip (optional but nice)
In `MemberCompanyData` / `PersonalInformation`:
private static $has_one = ['Bilder' => SilverStripeAssetsImage::class];
private static $owns = ['Bilder']; // auto-publishes with parent if versioned
You’re done—the image will render anywhere you output `$Bilder` (or `$Object.Bilder`) as above.
public function doStep5(HTTPRequest $request) {
$data = $request->postVars();
$sessionData = $this->getRequest()->getSession()->get('MBData');
if($sessionData['InseriereAls'] == 'broker'){
$session = $this->getRequest()->getSession();
$allData = array_merge(
$session->get('BrokerCompanyData') ?: [],
$data
);
$hash = $request->getSession()->get('HashValue');
$memberID = $this->getMemberIDByField('TempIDHash', $hash);
if ($memberID) {
$companyData = MemberCompanyData::get()->filter('MemberID', $memberID)->first();
}
if (empty($companyData) || !$companyData->exists()) {
$companyData = new MemberCompanyData();
$companyData->MemberID = $memberID;
}
foreach ($allData as $key => $value) {
if ($companyData->hasField($key)) {
$companyData->$key = $value;
}
}
if (!empty($data['Bilder'])) {
$companyData->ImageID = $data['Bilder'];
}
$companyData->write();
$session->clear('BrokerCompanyData');
return $this->redirect('/public/registration/step6');
}else{
$session = $this->getRequest()->getSession();
$allData = array_merge(
$session->get('PersonalInfo') ?: [],
$data
);
$hash = $request->getSession()->get('HashValue');
$memberID = $this->getMemberIDByField('TempIDHash', $hash);
if ($memberID) {
$personalInfo = PersonalInformation::get()->filter('MemberID', $memberID)->first();
}
if (empty($personalInfo) || !$personalInfo->exists()) {
$personalInfo = new PersonalInformation();
$personalInfo->MemberID = $memberID;
}
//var_dump($data);
if (!empty($data['Bilder'])) {
$personalInfo->ImageID = $data['Bilder'];
}
foreach ($allData as $key => $value) {
if ($personalInfo->hasField($key)) {
$personalInfo->$key = $value;
}
}
$personalInfo->write();
$session->clear('PersonalInfo');
// Login script start //
$authenticator = new MemberAuthenticator();
$userName = $request->getSession()->get('userName');
$userPsd = $request->getSession()->get('userPsd');
$member = $authenticator->authenticate([
'Email' => $userName,
'Password' => $userPsd
], $request);
if ($member && $member->exists()) {
$memberBasicData = MemberBasicData::get()->filter('MemberID', $member->ID)->first();
if ($memberBasicData) {
$InseriereAls = $memberBasicData->InseriereAls;
$firstName = $memberBasicData->FirstName;
$lastName = $memberBasicData->LastName;
$request->getSession()->set('UserType', $InseriereAls);
$request->getSession()->set('CUFirstName', $firstName);
$request->getSession()->set('CULastName', $lastName);
$identityStore = Injector::inst()->get(IdentityStore::class);
$identityStore->logIn($member, false, $request);
}
}
// Login script end //
return $this->redirect('/public/registration/thankyou');
}
}
### Prereqs (in both models)
// MemberCompanyData / PersonalInformation
use SilverStripeAssetsImage;
private static $has_one = [
'Bilder' => Image::class,
];
private static $owns = [
'Bilder',
];
Run `/dev/build?flush`.
—
### Controller helper (put inside your controller class)
use SilverStripeAssetsImage;
private function extractUploadID($value): int
{
if (!$value) return 0;
if (is_numeric($value)) return (int)$value;
if (is_array($value)) {
if (!empty($value['Files']) && is_array($value['Files'])) {
return (int)$value['Files'][0];
}
if (isset($value['id'])) return (int)$value['id'];
if (isset($value[0])) return (int)$value[0];
}
return 0;
}
### Updated `doStep5()` (only the changed bits)
public function doStep5(HTTPRequest $request) {
$data = $request->postVars();
$sessionData = $this->getRequest()->getSession()->get('MBData');
if ($sessionData['InseriereAls'] == 'broker') {
$session = $this->getRequest()->getSession();
$allData = array_merge($session->get('BrokerCompanyData') ?: [], $data);
$hash = $session->get('HashValue');
$memberID = $this->getMemberIDByField('TempIDHash', $hash);
if ($memberID) {
$companyData = MemberCompanyData::get()->filter('MemberID', $memberID)->first();
}
if (empty($companyData) || !$companyData->exists()) {
$companyData = new MemberCompanyData();
$companyData->MemberID = $memberID;
}
// Save other scalar fields
foreach ($allData as $key => $value) {
if ($companyData->hasField($key)) {
$companyData->$key = $value;
}
}
// Map form field "Bilder" -> relation "BilderID" and publish
$imgID = $this->extractUploadID($data['Bilder'] ?? null);
if ($imgID) {
if ($img = Image::get()->byID($imgID)) {
if ($img->isDraft()) {
$img->publishSingle();
}
$companyData->BilderID = $img->ID;
}
}
$companyData->write();
$session->clear('BrokerCompanyData');
return $this->redirect('/public/registration/step6');
} else {
$session = $this->getRequest()->getSession();
$allData = array_merge($session->get('PersonalInfo') ?: [], $data);
$hash = $session->get('HashValue');
$memberID = $this->getMemberIDByField('TempIDHash', $hash);
if ($memberID) {
$personalInfo = PersonalInformation::get()->filter('MemberID', $memberID)->first();
}
if (empty($personalInfo) || !$personalInfo->exists()) {
$personalInfo = new PersonalInformation();
$personalInfo->MemberID = $memberID;
}
// Save other scalar fields
foreach ($allData as $key => $value) {
if ($personalInfo->hasField($key)) {
$personalInfo->$key = $value;
}
}
// Map "Bilder" -> "BilderID" and publish
$imgID = $this->extractUploadID($data['Bilder'] ?? null);
if ($imgID) {
if ($img = Image::get()->byID($imgID)) {
if ($img->isDraft()) {
$img->publishSingle();
}
$personalInfo->BilderID = $img->ID;
}
}
$personalInfo->write();
$session->clear('PersonalInfo');
// (Your login script unchanged)
$authenticator = new MemberAuthenticator();
$member = $authenticator->authenticate([
'Email' => $session->get('userName'),
'Password' => $session->get('userPsd')
], $request);
if ($member && $member->exists()) {
$memberBasicData = MemberBasicData::get()->filter('MemberID', $member->ID)->first();
if ($memberBasicData) {
$request->getSession()->set('UserType', $memberBasicData->InseriereAls);
$request->getSession()->set('CUFirstName', $memberBasicData->FirstName);
$request->getSession()->set('CULastName', $memberBasicData->LastName);
$identityStore = Injector::inst()->get(IdentityStore::class);
$identityStore->logIn($member, false, $request);
}
}
return $this->redirect('/public/registration/thankyou');
}
}
### Frontend display
Anywhere in your template where you have the object:
<% if $Bilder %>
<img src="$Bilder.Fit(800,600).URL" alt="$Title.ATT">
<% end_if %>
This keeps your **form field name = `Bilder`**, saves to the **relation `Bilder`**, publishes the file, and makes it show up at `$Bilder.*` on the frontend.
## 1) Model: map the relation to `ImagedID` and add a `$Bilder` alias
use SilverStripeORMDataObject;
use SilverStripeAssetsImage;
class MemberCompanyData extends DataObject
{
// ... your $db etc.
private static $has_one = [
'Imaged' => Image::class, // matches column ImagedID
];
private static $owns = [
'Imaged',
];
// Optional: let templates use $Bilder even though DB is ImagedID
public function getBilder(): ?Image
{
return $this->Imaged();
}
}
Run `/dev/build?flush`.
## 2) Form field can stay as `Bilder`
UploadField::create('Bilder', 'Upload Image')
->setAllowedExtensions(['jpg','jpeg','png'])
->setIsMultiUpload(false)
->setAllowedMaxFileNumber(1)
->setFolderName('profiles')
->setAttribute('id', 'fileUpload')
->setAttribute('accept', 'image/jpeg, image/png')
->setAttribute('style', 'display:none;');
## 3) Controller: map `Bilder` → `ImagedID` and publish
Add this helper inside your controller (once):
use SilverStripeAssetsImage;
private function extractUploadID($value): int {
if (!$value) return 0;
if (is_numeric($value)) return (int)$value;
if (is_array($value)) {
if (!empty($value['Files'])) return (int)$value['Files'][0];
if (isset($value['id'])) return (int)$value['id'];
if (isset($value[0])) return (int)$value[0];
}
return 0;
}
Then in your `doStep5()` **broker** branch, replace your image bit with:
$imgID = $this->extractUploadID($data['Bilder'] ?? null);
if ($imgID) {
if ($img = Image::get()->byID($imgID)) {
if ($img->isDraft()) {
$img->publishSingle();
}
$companyData->ImagedID = $img->ID; // <-- map to ImagedID
}
}
(Keep the rest of your code as-is.)
If your **personal** branch also wants to store to `ImagedID`, do the same there; otherwise keep whatever column it already uses.
## 4) Frontend display
You now have two choices:
– Using the alias (cleanest in templates):
<% if $Bilder %>
<img src="$Bilder.Fit(800,600).URL" alt="$Title.ATT">
<% end_if %>
– Or directly by relation name:
<% if $Imaged %>
<img src="$Imaged.Fit(800,600).URL" alt="$Title.ATT">
<% end_if %>
That’s it — keep the form field name **`Bilder`**, save to **`ImagedID`**, publish the file, and render via `$Bilder` (alias) or `$Imaged`.
GET /public/dashboard/updateinfo/
Line 149 in /home/wmb/public_html/app/src/DashboardController.php
what if Fax is empty and optional in form but still says this error
TextField::create('Fax', 'Fax')->setAttribute('placeholder', 'Fax')->setValue($companyData['Fax']),
## Quick fix (one-liner)
TextField::create('Fax', 'Fax')
->setAttribute('placeholder', 'Fax')
->setValue($companyData['Fax'] ?? '');
`?? ''` prevents “Undefined array key 'Fax'”.
## If `$companyData` can be a DataObject (not an array)
$faxVal = is_array($companyData)
? ($companyData['Fax'] ?? '')
: (($companyData && $companyData->hasField('Fax')) ? ($companyData->Fax ?? '') : '');
TextField::create('Fax', 'Fax')
->setAttribute('placeholder', 'Fax')
->setValue($faxVal);
## Best practice (no `setValue` per field)
Let Silverstripe populate everything:
$fields = FieldList::create(
TextField::create('Fax', 'Fax')->setAttribute('placeholder','Fax'),
// ... other fields
);
$form = Form::create($this, 'UpdateInfoForm', $fields, $actions);
// Works with either a DataObject or an array:
if ($companyData) {
$form->loadDataFrom($companyData);
}
This avoids all “undefined key” warnings for optional fields.
if ($imgID) {
if ($img = Image::get()->byID($imgID)) {
if ($img->isDraft()) {
$img->publishSingle();
}
$companyData->ImageID = $img->ID;
}
}
this still not saving image
– the UploadField never posts an ID (AJAX upload didn’t run / token missing / field hidden),
– the posted value shape isn’t what you expect,
– or you’re not handling the raw `$_FILES` fallback.
Here’s a robust, drop-in fix that:
1) reads the ID from `Bilder` when UploadField posts it, and
2) falls back to creating the `Image` from `$_FILES` if no ID was posted.
Use this with your existing **has_one `Image`** (so DB column = `ImageID`) and form field name **`Bilder`**.
—
### 1) Add helpers in your controller
use SilverStripeAssetsImage;
use SilverStripeAssetsUpload;
/**
* Extract an uploaded file ID from UploadField’s various post shapes.
*/
private function extractUploadID($value): int {
if (!$value) return 0;
if (is_numeric($value)) return (int)$value;
if (is_array($value)) {
if (!empty($value['Files']) && is_array($value['Files'])) {
return (int)$value['Files'][0];
}
if (isset($value['id'])) return (int)$value['id'];
if (isset($value[0])) return (int)$value[0];
}
return 0;
}
/**
* Ensure we get an Image either from posted ID or raw $_FILES (fallback).
*/
private function getImageFromRequest(SilverStripeControlHTTPRequest $request, $postedValue, string $folder = 'profiles'): ?Image {
// 1) Try ID from UploadField
$id = $this->extractUploadID($postedValue);
if ($id) {
if ($img = Image::get()->byID($id)) {
return $img;
}
}
// 2) Fallback to raw files if AJAX attach didn’t happen
$files = $request->getFiles();
$uf = $files['Bilder'] ?? null; // name matches your field
if ($uf) {
// UploadField usually nests as Bilder[Files][0]
if (isset($uf['tmp_name']['Files'][0])) {
$flat = [
'name' => $uf['name']['Files'][0],
'type' => $uf['type']['Files'][0],
'tmp_name' => $uf['tmp_name']['Files'][0],
'error' => $uf['error']['Files'][0],
'size' => $uf['size']['Files'][0],
];
} elseif (isset($uf['tmp_name']) && is_string($uf['tmp_name'])) {
// Simple (non-nested) case
$flat = $uf;
} else {
$flat = null;
}
if ($flat && is_uploaded_file($flat['tmp_name'])) {
$upload = Upload::create();
$upload->setAllowedExtensions(['jpg','jpeg','png','webp']);
$image = Image::create();
// This writes the file record + moves the file to assets/$folder
if ($upload->loadIntoFile($flat, $image, $folder)) {
return $image;
}
}
}
return null;
}
—
### 2) Use it inside your `doStep5()` where you save the company data
Replace your current image block with this (broker branch shown; repeat similarly for personal):
// Map + publish image from form field "Bilder"
if ($image = $this->getImageFromRequest($request, $data['Bilder'] ?? null, 'profiles')) {
if ($image->isDraft()) {
$image->publishSingle();
}
$companyData->ImageID = $image->ID; // your has_one is 'Image' => Image::class
}
Do the same in the personal branch if you also save there:
if ($image = $this->getImageFromRequest($request, $data['Bilder'] ?? null, 'profiles')) {
if ($image->isDraft()) $image->publishSingle();
$personalInfo->ImageID = $image->ID;
}
Then `write()` the record as you already do.
—
### 3) Frontend checklist (prevents silent failures)
– Make sure the form includes the security token **if you render fields manually**:
<form $Form.AttributesHTML enctype="multipart/form-data">
$Form.SecurityToken
<!-- your fields -->
</form>
– Keep `UploadField` **in the form DOM**. Avoid `style="display:none"` on the whole wrapper; prefer visually hiding via CSS (opacity/position) so the UploadField JS can initialize:
.upload-hidden { position:absolute; left:-9999px; width:1px; height:1px; overflow:hidden; }
And in PHP:
UploadField::create('Bilder', 'Upload Image')
->setIsMultiUpload(false)
->setAllowedMaxFileNumber(1)
->setAllowedExtensions(['jpg','jpeg','png'])
->setFolderName('profiles')
->setAttribute('id', 'fileUpload')
->addExtraClass('upload-hidden');
– Ensure your label/button triggers the **real** file input inside the UploadField (as we wired earlier).
—
### 4) Displaying the image
Once saved, show it in templates:
<% if $Image %>
<img src="$Image.Fit(800,600).URL" alt="$Firmaname.ATT">
<% end_if %>
—
This approach makes your save path resilient: it uses the ID when UploadField’s AJAX succeeds, and seamlessly falls back to processing the raw upload when it doesn’t—so the image always gets stored, published, and linked to `ImageID`.
<script>
const fileInput = document.getElementById('fileUpload');
const fileName = document.getElementById('file-name');
fileInput.addEventListener('change', function () {
if (this.files.length > 0) {
fileName.textContent = this.files[0].name;
} else {
fileName.textContent = 'Bilder';
}
});
</script>
$Description
$Form.Actions
enctype="multipart/form-data" is not aplying in front end its replaced by enctype="application/x-www-form-urlencoded"
Do one of these:
# Option A (recommended): use `$Form.OpenTag` / `$Form.CloseTag`
Silverstripe will output the correct `enctype` if you set it on the form.
$Form.OpenTag
<!-- your custom layout -->
$Bilder
$Description
$Form.Actions
$Form.CloseTag
In your `Step5Form()`:
public function Step5Form()
{
$fields = FieldList::create(
UploadField::create('Bilder', 'Upload Image')
->setAllowedExtensions(['jpg','jpeg','png'])
->setIsMultiUpload(false)
->setAllowedMaxFileNumber(1)
->setFolderName('profiles'),
// other fields...
);
$actions = FieldList::create(
FormAction::create('doStep5', 'Weiter')
);
$form = Form::create($this, 'Step5Form', $fields, $actions);
// Set enctype on the form object:
if (method_exists($form, 'setEncType')) {
$form->setEncType('multipart/form-data'); // SS4/SS5
} else {
$form->setAttribute('enctype', 'multipart/form-data');
}
return $form;
}
# Option B: keep your manual “
If you must keep:
<form $Form.AttributesHTML>
$Form.SecurityToken
$Bilder
$Description
$Form.Actions
</form>
Then set the enctype **server-side** exactly as above in `Step5Form()`:
$form->setEncType('multipart/form-data'); // or ->setAttribute('enctype', 'multipart/form-data')
Don’t add another `enctype` in the template—`$Form.AttributesHTML` already prints one.
—
## Quick checks
– Ensure the upload field is actually inside the form DOM you submit.
– Include `$Form.SecurityToken` if you use a manual “; missing token can block UploadField’s attach.
– Avoid `display:none` on the entire UploadField wrapper; prefer off-screen CSS if you’re hiding it, so its JS can initialise.
After this change, view source: you should see `enctype="multipart/form-data"` on the rendered “, and uploads will post correctly.
die;
SHowing empty array
Here are two solid ways forward—pick one:
# Option A — Keep `UploadField` (recommended)
Use the posted ID(s), then publish and save.
1) Make sure your form is built like this (server-side):
$form = Form::create($this, 'Step5Form', $fields, $actions);
$form->setEncType('multipart/form-data'); // important
…and render with `$Form.OpenTag` / `$Form.CloseTag` **or** include `$Form.SecurityToken` if you use a manual “ tag.
2) In your controller, read the ID from `$data['Bilder']` and map it to your relation:
use SilverStripeAssetsImage;
private function extractUploadID($value): int {
if (!$value) return 0;
if (is_numeric($value)) return (int)$value;
if (is_array($value)) {
if (!empty($value['Files']) && is_array($value['Files'])) return (int)$value['Files'][0];
if (isset($value['id'])) return (int)$value['id'];
if (isset($value[0])) return (int)$value[0];
}
return 0;
}
$imgID = $this->extractUploadID($data['Bilder'] ?? null);
if ($imgID) {
if ($img = Image::get()->byID($imgID)) {
if ($img->isDraft()) {
$img->publishSingle();
}
$companyData->ImageID = $img->ID; // or ImagedID if that’s your column
}
}
3) Quick sanity checks if you still get nothing in `$data['Bilder']`:
– Ensure `$Bilder` is **inside** the form that’s being submitted.
– Include `$Form.SecurityToken` in your template if using a manual “.
– Don’t hide the UploadField wrapper with `display:none;` before it initializes. If you must hide, move it off-screen with CSS (so Dropzone JS can still attach).
– In DevTools → Network, after selecting a file, you should see an **XHR** to an assets/upload endpoint (200 OK). If it’s **403**, you likely need to allow front-end uploads (see note below).
> ⚠️ Front-end permission note: by default, anonymous users may be blocked from uploading. If your XHR is 403, add a small extension to let front-end users create files, or handle uploads via Option B below.
# Option B — Use `FileField` (classic non-AJAX)
If you prefer raw `$_FILES` on submit, replace `UploadField` with `FileField`:
**Form**
use SilverStripeFormsFileField;
FileField::create('Bilder', 'Upload Image')
**Controller**
use SilverStripeAssetsImage;
use SilverStripeAssetsUpload;
$files = $request->getFiles();
if (!empty($files['Bilder']['tmp_name'])) {
$upload = Upload::create();
$upload->setAllowedExtensions(['jpg','jpeg','png','webp']);
$image = Image::create();
if ($upload->loadIntoFile($files['Bilder'], $image, 'profiles')) {
$image->publishSingle();
$companyData->ImageID = $image->ID; // or ImagedID
} else {
// handle $upload->getErrors()
}
}
# Showing the image on the front end
Once saved:
<% if $Image %>
<img src="$Image.Fit(800,600).URL" alt="$Firmaname.ATT">
<% end_if %>
(or `$Imaged` / `$Bilder` if you use those names)
—
## If your UploadField AJAX is failing (403)
Allow front-end uploads by granting create permission on `File` (scoped as you see fit):
**app/src/Extensions/FrontendFileCreateExtension.php**
namespace AppExtensions;
use SilverStripeCoreExtension;
class FrontendFileCreateExtension extends Extension
{
public function canCreate($member = null, $context = [])
{
// TODO: tighten this to your registration route/session check
return true;
}
}
**app/_config/app.yml**
SilverStripeAssetsFile:
extensions:
- AppExtensionsFrontendFileCreateExtension
Then `?flush`.
—
**Bottom line:** With UploadField, expect `$_FILES` to be empty on submit—use the posted **ID** in `$data['Bilder']`. If you need `$_FILES`, switch to `FileField`.
## 1) Build the form with `FileField` and set the enctype
use SilverStripeFormsFieldList;
use SilverStripeFormsForm;
use SilverStripeFormsFormAction;
use SilverStripeFormsFileField;
use SilverStripeFormsTextAreaField; // example for Description
public function Step5Form()
{
$fields = FieldList::create(
FileField::create('Bilder', 'Upload Image')
->setAttribute('id', 'bilderFile') // so a <label for="bilderFile"> works
->setAttribute('accept', 'image/jpeg,image/png'),
TextAreaField::create('Description', 'Beschreibung')
);
$actions = FieldList::create(
FormAction::create('doStep5', 'Weiter')
);
$form = Form::create($this, 'Step5Form', $fields, $actions);
// IMPORTANT: classic uploads need multipart/form-data
if (method_exists($form, 'setEncType')) {
$form->setEncType('multipart/form-data');
} else {
$form->setAttribute('enctype', 'multipart/form-data');
}
return $form;
}
### Template (use the form’s own tags)
$Form.OpenTag
$Bilder
$Description
$Form.Actions
$Form.CloseTag
If you keep a manual “, include `$Form.SecurityToken` inside it. Don’t add your own enctype; it’s already set on the form.
—
## 2) Save the file in `doStep5()` (classic way)
Use `Upload::loadIntoFile()` with the raw `$_FILES['Bilder']`, then publish and attach to your record.
use SilverStripeAssetsImage;
use SilverStripeAssetsUpload;
private function attachImageFromFiles(array $files, string $fieldName = 'Bilder', string $folder = 'profiles'): ?Image
{
if (empty($files[$fieldName]) || empty($files[$fieldName]['tmp_name'])) {
return null; // nothing uploaded
}
$upload = Upload::create();
$upload->setAllowedExtensions(['jpg','jpeg','png','webp']); // adjust if needed
$image = Image::create();
if ($upload->loadIntoFile($files[$fieldName], $image, $folder)) {
return $image; // not published yet
}
// Optionally log or surface $upload->getErrors()
return null;
}
private function setImageRelation($record, Image $img): void
{
// If your DB column is ImageID (from has_one 'Image' => Image::class)
if ($record->hasField('ImageID')) {
$record->ImageID = $img->ID;
return;
}
// If your DB column is ImagedID (older schema)
if ($record->hasField('ImagedID')) {
$record->ImagedID = $img->ID;
return;
}
// If you actually have a has_one Bilder => Image::class
if ($record->hasField('BilderID')) {
$record->BilderID = $img->ID;
return;
}
}
### Drop this into your existing `doStep5()` at the appropriate spots
**Broker branch (MemberCompanyData):**
$files = $request->getFiles();
if ($img = $this->attachImageFromFiles($files, 'Bilder', 'profiles')) {
$img->publishSingle();
$this->setImageRelation($companyData, $img); // sets ImageID/ImagedID/BilderID as available
}
$companyData->write();
**Personal branch (PersonalInformation):**
$files = $request->getFiles();
if ($img = $this->attachImageFromFiles($files, 'Bilder', 'profiles')) {
$img->publishSingle();
$this->setImageRelation($personalInfo, $img);
}
$personalInfo->write();
> This path does **not** rely on UploadField’s AJAX. You’ll now see `$_FILES['Bilder']` populated, and the image will be created in `assets/profiles/`, published, and linked to your record.
—
## 3) Show it on the frontend
Given your current model (`MemberCompanyData` has `has_one 'Image' => Image::class`):
<% if $Image %>
<img src="$Image.Fit(800,600).URL" alt="$Firmaname.ATT">
<% end_if %>
If your DB actually uses `ImagedID`, either render `$Imaged` or keep a small alias:
public function getBilder() { return $this->Image ?: $this->Imaged(); }
then in template:
<% if $Bilder %><img src="$Bilder.Fit(800,600).URL" alt="$Firmaname.ATT"><% end_if %>
—
## Quick gotchas checklist
– Make sure `php.ini` allows the sizes you need (`upload_max_filesize`, `post_max_size`).
– If the field is **optional**, this code safely skips when no file is chosen.
– If you previously had `UploadField` JS in the page, remove it (no longer needed).
That’s it—classic upload enabled, saved, published, and ready to display.
For **classic uploads**, just read from PHP’s `$_FILES`.
Here’s the minimal, working setup:
## 1) Use `FileField` in the form and set enctype
use SilverStripeFormsFileField;
public function Step5Form() {
// ...
$fields = FieldList::create(
FileField::create('Bilder', 'Upload Image')
->setAttribute('accept', 'image/jpeg,image/png')
);
// ...
$form = Form::create($this, 'Step5Form', $fields, $actions);
$form->setEncType('multipart/form-data');
return $form;
}
## 2) In `doStep5(…)`, read `$_FILES` (no getFiles)
Keep your current signature if you want (`HTTPRequest $request`)—just don’t call `getFiles()`.
use SilverStripeAssetsImage;
use SilverStripeAssetsUpload;
private function attachImageFromFiles(string $field = 'Bilder', string $folder = 'profiles'): ?Image
{
if (empty($_FILES[$field]) || empty($_FILES[$field]['tmp_name'])) {
return null; // no file chosen
}
$upload = Upload::create();
$upload->setAllowedExtensions(['jpg','jpeg','png','webp']);
$image = Image::create();
if ($upload->loadIntoFile($_FILES[$field], $image, $folder)) {
return $image; // not published yet
}
// Optional: inspect $upload->getErrors()
return null;
}
Use it in both branches:
// broker branch
if ($img = $this->attachImageFromFiles('Bilder', 'profiles')) {
$img->publishSingle();
// set the correct relation column you have:
if ($companyData->hasField('ImageID')) $companyData->ImageID = $img->ID;
if ($companyData->hasField('ImagedID')) $companyData->ImagedID = $img->ID;
if ($companyData->hasField('BilderID')) $companyData->BilderID = $img->ID;
}
$companyData->write();
// personal branch
if ($img = $this->attachImageFromFiles('Bilder', 'profiles')) {
$img->publishSingle();
if ($personalInfo->hasField('ImageID')) $personalInfo->ImageID = $img->ID;
if ($personalInfo->hasField('ImagedID')) $personalInfo->ImagedID = $img->ID;
if ($personalInfo->hasField('BilderID')) $personalInfo->BilderID = $img->ID;
}
$personalInfo->write();
## 3) Frontend display
<% if $Image %>
<img src="$Image.Fit(800,600).URL" alt="$Firmaname.ATT">
<% end_if %>
(or `$Imaged` / `$Bilder` depending on your relation)
### Quick checklist
– Ensure the rendered form uses `$Form.OpenTag`/`$Form.CloseTag` **or** includes `$Form.SecurityToken` if you output a manual “.
– Confirm the HTML shows `enctype="multipart/form-data"`.
– Remove any old `UploadField` JS; it’s not needed for classic uploads.
This removes the `getFiles()` call entirely and gets your classic upload working.
POST /public/registration/doStep5
Line 57 in /home/wmb/public_html/vendor/silverstripe/framework/src/Core/CustomMethods.php
### Fix: replace this
$upload = Upload::create();
$upload->setAllowedExtensions(['jpg','jpeg','png','webp']); // ❌ causes BadMethodCallException
### With this
use SilverStripeAssetsImage;
use SilverStripeAssetsUpload;
$upload = Upload::create();
$upload->getValidator()->setAllowedExtensions(['jpg','jpeg','png','webp']); // ✅
### Full classic-upload snippet (drop-in)
private function attachImageFromFiles(string $field = 'Bilder', string $folder = 'profiles'): ?Image
{
if (empty($_FILES[$field]) || empty($_FILES[$field]['tmp_name'])) {
return null;
}
$upload = Upload::create();
$upload->getValidator()->setAllowedExtensions(['jpg','jpeg','png','webp']);
$image = Image::create();
if ($upload->loadIntoFile($_FILES[$field], $image, $folder)) {
return $image; // not published yet
}
// Optional: inspect $upload->getErrors() for why it failed
return null;
}
Then in `doStep5()`:
if ($img = $this->attachImageFromFiles('Bilder', 'profiles')) {
$img->publishSingle();
// set whichever relation column you actually have:
if ($companyData->hasField('ImageID')) $companyData->ImageID = $img->ID;
if ($companyData->hasField('ImagedID')) $companyData->ImagedID = $img->ID;
if ($companyData->hasField('BilderID')) $companyData->BilderID = $img->ID;
}
$companyData->write();
### (Optional) Site-wide config instead of per-request
# app/_config/assets.yml
SilverStripeAssetsFile:
allowed_extensions:
- jpg
- jpeg
- png
- webp
Then you can skip the validator line entirely.
This change removes the exception and gets your classic file upload working.
## 1) Make sure the relation exists
You already have it:
// MemberCompanyData
private static $has_one = [
'Image' => SilverStripeAssetsImage::class,
];
private static $owns = ['Image']; // optional but nice
## 2) Pass the record to your template
Example (e.g., in `step6()` or wherever you render the confirmation page):
use SilverStripeSecuritySecurity;
public function step6()
{
$memberID = Security::getCurrentUser()?->ID; // or however you resolve it
$company = AppModelMemberCompanyData::get()
->filter('MemberID', $memberID)
->first();
return $this->customise(['Company' => $company])
->renderWith(['Layout/RegistrationStep6','Page']);
}
## 3) Render in the `.ss` template
<% if $Company && $Company.Image %>
<img
src="$Company.Image.ScaleMax(800,600).URL"
alt="$Company.Firmaname.ATT">
<% else %>
<p>No image uploaded.</p>
<% end_if %>
### Popular variants
– Original: `$Company.Image.URL`
– Crop to exact size: `$Company.Image.Fill(600,400).URL`
– Fit inside box: `$Company.Image.Fit(600,400).URL`
– Max bounds keep ratio: `$Company.Image.ScaleMax(1200,800).URL`
### Simple responsive example
<% if $Company.Image %>
<img
src="$Company.Image.ScaleMax(600,400).URL"
srcset="
$Company.Image.ScaleMax(600,400).URL 600w,
$Company.Image.ScaleMax(900,600).URL 900w,
$Company.Image.ScaleMax(1200,800).URL 1200w"
sizes="(max-width: 700px) 100vw, 700px"
alt="$Company.Firmaname.ATT">
<% end_if %>
## 4) If your DB column is different (e.g., `ImagedID`)
Either:
– change the relation to match:
private static $has_one = ['Imaged' => Image::class];
and in template use `$Company.Imaged…`, **or**
– add an alias so templates can still use `$Image`:
public function getImage() { return $this->Imaged(); }
As long as the relation exists and the file is published (you already call `publishSingle()`), `$Company.Image.*` will work on the frontend.
public function updateinfo(HTTPRequest $request){
$firstName = $request->getSession()->get('CUFirstName');
$lastName = $request->getSession()->get('CULastName');
$form = $this->UpdateCompanyForm();
$session = $this->getRequest()->getSession();
$formSuccess = $session->get('FormSuccess');
$errorMessage = $session->get('FormError');
$session->clear('FormError');
$session->clear('FormSuccess');
return $this->customise([
'Title' => 'Broker-Informationen',
'Name' => $firstName.' '.$lastName,
'FormError' => $errorMessage,
'FormSuccess' => $formSuccess,
'Form' => $form,
'MemberID' => $form->Fields()->dataFieldByName('MemberID'),
'Firmaname' => $form->Fields()->dataFieldByName('Firmaname'),
'HRBHRA' => $form->Fields()->dataFieldByName('HRBHRA'),
'Address' => $form->Fields()->dataFieldByName('Address'),
'CountryCode' => $form->Fields()->dataFieldByName('CountryCode'),
'Telefon' => $form->Fields()->dataFieldByName('Telefon'),
'Email' => $form->Fields()->dataFieldByName('Email'),
'Website' => $form->Fields()->dataFieldByName('Website'),
'Fax' => $form->Fields()->dataFieldByName('Fax'),
'Description' => $form->Fields()->dataFieldByName('Description')
])->renderWith(['Layout/Broker/UpdateInfo', 'Page']);
}
public function UpdateCompanyForm(){
$member = Security::getCurrentUser();
$records = MemberCompanyData::get()
->filter('MemberID', $member->ID)
->first();
if ($records) {
$companyData = $records->toMap();
}
$fields = FieldList::create(
HiddenField::create('MemberID', 'MemberID')->setValue($member->ID),
TextField::create('Firmaname', 'Firmaname')->setAttribute('placeholder', 'Firmaname*')->setValue($companyData['Firmaname']),
TextField::create('HRBHRA', 'HRB / HRA')->setAttribute('placeholder', 'HRB / HRA*')->setValue($companyData['HRBHRA']),
TextareaField::create('Address', 'Adresse')->setAttribute('placeholder', 'Adresse*')->setValue($companyData['Firmaname']),
DropdownField::create('CountryCode', 'Country Code', [
'+49' => 'Deutschland (+49)',
'+43' => 'Österreich (+43)',
'+41' => 'Schweiz (+41)'
])->setValue('+49')->setEmptyString('Ländervorwahl auswählen*')->setValue($companyData['CountryCode']),
TextField::create('Telefon', 'Telefon')->setAttribute('placeholder', 'Telefon*')->setValue($companyData['Telefon']),
EmailField::create('Email', 'Emailadresse')->setAttribute('placeholder', 'Emailadresse*')->setValue($companyData['Email']),
TextField::create('Website', 'Website')->setAttribute('placeholder', 'Website')->setValue($companyData['Website']),
TextField::create('Fax', 'Fax')->setAttribute('placeholder', 'Fax')->setValue($companyData['Fax'] ?? ''),
TextareaField::create('Description', 'Company Description')->setAttribute('placeholder', '')->addExtraClass('short_content')->setValue($companyData['Description']),
);
$actions = FieldList::create(
FormAction::create('doRegister', 'um zu sehen')
);
$validator = RequiredFields::create(['Firmaname', 'HRBHRA', 'Address', 'CountryCode', 'Telefon', 'Email']);
$form = Form::create($this, 'UpdateCompanyForm', $fields, $actions, $validator);
$form->setFormAction('/public/dashboard/doUpdateCompany');
return $form;
}
## 1) Update the form to (a) show current image preview, (b) allow replacing it, and (c) set `enctype`
use SilverStripeFormsFieldList;
use SilverStripeFormsForm;
use SilverStripeFormsFormAction;
use SilverStripeFormsHiddenField;
use SilverStripeFormsTextField;
use SilverStripeFormsTextareaField;
use SilverStripeFormsEmailField;
use SilverStripeFormsDropdownField;
use SilverStripeFormsFileField;
use SilverStripeFormsRequiredFields;
use SilverStripeSecuritySecurity;
public function UpdateCompanyForm()
{
$member = Security::getCurrentUser();
$record = AppModelMemberCompanyData::get()->filter('MemberID', $member->ID)->first();
$fields = FieldList::create(
HiddenField::create('MemberID')->setValue($member->ID),
// — Optional: current image preview (if any)
SilverStripeFormsLiteralField::create(
'ImagePreview',
$record && $record->ImageID
? sprintf(
'<div class="mb-3"><img src="%s" alt="Current image" style="max-width:160px;height:auto;border-radius:8px;" /></div>',
$record->Image()->ScaleMax(320, 320)->URL()
)
: '<div class="mb-3" style="color:#666;">(Kein Bild hochgeladen)</div>'
),
// — Classic file input to upload/replace
FileField::create('Bilder', 'Neues Bild hochladen')
->setAttribute('accept', 'image/jpeg,image/png,image/webp'),
TextField::create('Firmaname', 'Firmaname')->setAttribute('placeholder', 'Firmaname*'),
TextField::create('HRBHRA', 'HRB / HRA')->setAttribute('placeholder', 'HRB / HRA*'),
TextareaField::create('Address', 'Adresse')->setAttribute('placeholder', 'Adresse*'),
DropdownField::create('CountryCode', 'Country Code', [
'+49' => 'Deutschland (+49)',
'+43' => 'Österreich (+43)',
'+41' => 'Schweiz (+41)'
])->setEmptyString('Ländervorwahl auswählen*'),
TextField::create('Telefon', 'Telefon')->setAttribute('placeholder', 'Telefon*'),
EmailField::create('Email', 'Emailadresse')->setAttribute('placeholder', 'Emailadresse*'),
TextField::create('Website', 'Website')->setAttribute('placeholder', 'Website'),
TextField::create('Fax', 'Fax')->setAttribute('placeholder', 'Fax'),
TextareaField::create('Description', 'Company Description')->addExtraClass('short_content')
);
$actions = FieldList::create(FormAction::create('doUpdateCompany', 'Speichern'));
$validator = RequiredFields::create(['Firmaname','HRBHRA','Address','CountryCode','Telefon','Email']);
$form = Form::create($this, 'UpdateCompanyForm', $fields, $actions, $validator);
$form->setEncType('multipart/form-data'); // ← important for classic
$form->setFormAction('/public/dashboard/doUpdateCompany');
// Let Silverstripe populate values safely (no undefined index warnings):
if ($record) {
$form->loadDataFrom($record);
} else {
// Sensible defaults
$form->setData(['CountryCode' => '+49']);
}
return $form;
}
> Note: You had a small typo before: `Address` was being set from `Firmaname`. Using `loadDataFrom($record)` avoids all those per-field `setValue()` issues.
## 2) Save handler: update fields + handle classic file upload + publish
Add this action (path must match your `setFormAction`):
use SilverStripeAssetsUpload;
use SilverStripeAssetsImage;
use SilverStripeSecuritySecurity;
public function doUpdateCompany(SilverStripeControlHTTPRequest $request)
{
$member = Security::getCurrentUser();
if (!$member) { return $this->httpError(403); }
$record = AppModelMemberCompanyData::get()->filter('MemberID', $member->ID)->first()
?: AppModelMemberCompanyData::create(['MemberID' => $member->ID]);
// 1) Update scalar fields from POST safely
$form = $this->UpdateCompanyForm(); // reuse field/validation context
$form->loadDataFrom($request->postVars());
$form->saveInto($record);
// 2) Classic file upload (optional field)
if (!empty($_FILES['Bilder']) && !empty($_FILES['Bilder']['tmp_name'])) {
$upload = Upload::create();
$upload->getValidator()->setAllowedExtensions(['jpg','jpeg','png','webp']);
$image = Image::create();
if ($upload->loadIntoFile($_FILES['Bilder'], $image, 'profiles')) {
$image->publishSingle(); // make it visible to guests
$record->ImageID = $image->ID; // your has_one is 'Image'
} else {
// You can surface $upload->getErrors() if needed
}
}
$record->write();
// Flash + redirect
$session = $request->getSession();
$session->set('FormSuccess', 'Daten gespeichert.');
return $this->redirect('/public/dashboard/updateinfo');
}
## 3) Template
Render the form using its own tags so the right `enctype` is output:
$Form.OpenTag
$Form.Fields
$Form.Actions
$Form.CloseTag
If you prefer your custom layout, still use `$Form.OpenTag`/`$Form.CloseTag` and place fields individually:
$Form.OpenTag
$ImagePreview <!-- LiteralField -->
$Bilder <!-- FileField -->
$Firmaname
$HRBHRA
$Address
$CountryCode
$Telefon
$Email
$Website
$Fax
$Description
$Form.Actions
$Form.CloseTag
## 4) Show image anywhere on frontend
Given your `has_one 'Image'`:
<% if $Image %>
<img src="$Image.Fit(800,600).URL" alt="$Firmaname.ATT">
<% end_if %>
That’s it: the update screen shows the current image, lets users upload a new one, and persists + publishes it.
public function updateinfo(HTTPRequest $request){
$firstName = $request->getSession()->get('CUFirstName');
$lastName = $request->getSession()->get('CULastName');
$form = $this->UpdateCompanyForm();
$session = $this->getRequest()->getSession();
$formSuccess = $session->get('FormSuccess');
$errorMessage = $session->get('FormError');
$session->clear('FormError');
$session->clear('FormSuccess');
return $this->customise([
'Title' => 'Broker-Informationen',
'Name' => $firstName.' '.$lastName,
'FormError' => $errorMessage,
'FormSuccess' => $formSuccess,
'Form' => $form,
'MemberID' => $form->Fields()->dataFieldByName('MemberID'),
'Firmaname' => $form->Fields()->dataFieldByName('Firmaname'),
'HRBHRA' => $form->Fields()->dataFieldByName('HRBHRA'),
'Address' => $form->Fields()->dataFieldByName('Address'),
'CountryCode' => $form->Fields()->dataFieldByName('CountryCode'),
'Telefon' => $form->Fields()->dataFieldByName('Telefon'),
'Email' => $form->Fields()->dataFieldByName('Email'),
'Website' => $form->Fields()->dataFieldByName('Website'),
'Fax' => $form->Fields()->dataFieldByName('Fax'),
'Description' => $form->Fields()->dataFieldByName('Description')
])->renderWith(['Layout/Broker/UpdateInfo', 'Page']);
}
## Controller (updateinfo)
Add the company fetch and pass it to `customise`:
use SilverStripeSecuritySecurity;
use AppModelMemberCompanyData;
public function updateinfo(HTTPRequest $request)
{
$firstName = $request->getSession()->get('CUFirstName');
$lastName = $request->getSession()->get('CULastName');
$form = $this->UpdateCompanyForm();
$session = $this->getRequest()->getSession();
$formSuccess = $session->get('FormSuccess');
$errorMessage = $session->get('FormError');
$session->clear('FormError');
$session->clear('FormSuccess');
$member = Security::getCurrentUser();
$company = MemberCompanyData::get()->filter('MemberID', $member->ID)->first();
return $this->customise([
'Title' => 'Broker-Informationen',
'Name' => $firstName.' '.$lastName,
'FormError' => $errorMessage,
'FormSuccess' => $formSuccess,
'Form' => $form,
// existing field handles...
'MemberID' => $form->Fields()->dataFieldByName('MemberID'),
'Firmaname' => $form->Fields()->dataFieldByName('Firmaname'),
'HRBHRA' => $form->Fields()->dataFieldByName('HRBHRA'),
'Address' => $form->Fields()->dataFieldByName('Address'),
'CountryCode' => $form->Fields()->dataFieldByName('CountryCode'),
'Telefon' => $form->Fields()->dataFieldByName('Telefon'),
'Email' => $form->Fields()->dataFieldByName('Email'),
'Website' => $form->Fields()->dataFieldByName('Website'),
'Fax' => $form->Fields()->dataFieldByName('Fax'),
'Description' => $form->Fields()->dataFieldByName('Description'),
// add the record so template can access $Company.Image
'Company' => $company,
])->renderWith(['Layout/Broker/UpdateInfo', 'Page']);
}
## Template (`Layout/Broker/UpdateInfo.ss`)
Where you want the image to appear:
<% if $Company && $Company.Image %>
<img src="$Company.Image.ScaleMax(800,600).URL" alt="$Company.Firmaname.ATT">
<% end_if %>
(Use `.Fill(w,h)`, `.Fit(w,h)`, or `.URL` as you prefer.)
> Note: Make sure you publish the file when saving (you already call `publishSingle()`); otherwise anonymous users won’t see it.
but this $Company.Image.ScaleMax(800,600).URL not get url
## Use `Link` (or `AbsoluteURL`) on the resized variant
Sometimes `.URL` on a *manipulated* image returns empty depending on setup. Use `.Link` instead.
<% if $Company.Image %>
<% with $Company.Image %>
<img src="$ScaleMax(800,600).Link" alt="$Up.Firmaname.ATT">
<% end_with %>
<% end_if %>
Variants you can try:
– Original: `$Company.Image.URL` (or `$Company.Image.Link`)
– Resized: `$Company.Image.ScaleMax(800,600).Link`
– Absolute: `$Company.Image.ScaleMax(800,600).AbsoluteURL`
## Defensive fallback (if manipulation fails)
<% if $Company.Image %>
<% with $Company.Image %>
<% if $ScaleMax(800,600) %>
<img src="$ScaleMax(800,600).Link" alt="$Up.Firmaname.ATT">
<% else %>
<img src="$Link" alt="$Up.Firmaname.ATT">
<% end_if %>
<% end_with %>
<% end_if %>
## If it’s still blank
– Ensure the image is **published** (you’re calling `publishSingle()` — good).
– Make sure PHP has **GD or Imagick** enabled; manipulations need one of them. Without it, `$ScaleMax(…)` can be empty.
– `?flush` after changing code/config, and (if needed) clear generated variants (`assets/_resampled` in SS4, or let SS regenerate).
This should give you a working resized URL on the frontend.