Saving uploaded file to DB in custom handler - silverstripe

I have a DataObject called Document:
class Document extends DataObject {
private static $db = array(
'Name' => "varchar"
);
private static $has_one = array(
'Document' => 'File'
);
}
And have put this on a front end form. The file uploads correctly and I'd like to save the details to the db.
public function submitDocument($data, $form) {
$document = new Document();
$document->Name = $data['Name'];
$document->Document = $data['Document'];
$document->write();
}
Now the Name gets written to the DB however the document doesn't (it's in the file system). I know I can just write the whole thing because the names of the fields are the same in the database however I'd like to write it manually as I'm wanting to do more complex things. I've looked at the attribute that comes in as:{"Files":["19"]} so I've also tried $data['Document']['Files'][0] which logs in my test as 20 however it's still not saved. I've also manually tried with just 20 for testing and that doesn't work either. How do I save it?

public function submitDocument($data, $form) {
$document = new Document();
$document->Name = $data['Name'];
$document->DocumentID = $data['Document']['Files'][0];
$document->write();
}

Related

How to override the PUT operation update process in the Api Platform

I'm trying to override the PUT operation to perform my actions under certain conditions. That is, if the sent object is different from the original object (from the database), then I need to create a new object and return it without changing the original object.
Now when I execute the query I get a new object, as expected, but the problem is that the original object also changes
Entity
#[ApiResource(
operations: [
new Get(),
new GetCollection(),
new Post(controller: CreateAction::class),
new Put(processor: EntityStateProcessor::class),
],
paginationEnabled: false
)]
class Entity
EntityStateProcessor
final class PageStateProcessor implements ProcessorInterface
{
private ProcessorInterface $decorated;
private EntityCompare $entityCompare;
public function __construct(ProcessorInterface $decorated, EntityCompare $entityCompare)
{
$this->decorated = $decorated;
$this->entityCompare = $entityCompare;
}
public function process($data, Operation $operation, array $uriVariables = [], array $context = [])
{
if (($this->entityCompare)($data)) { // checking for object changes
$new_entity = clone $data; // (without id)
// do something with new entity
return $this->decorated->process($new_entity, $operation, $uriVariables, $context);
}
return $data;
}
}
I don't understand why this happens, so I return a clone of the original object to the process. It would be great if someone could tell me what my mistake is.
I also tried the following before returning the process
$this->entityManager->refresh($data); - Here I assumed that the original instance of the object will be updated with data from the database and the object will not be updated with data from the query
$this->entityManager->getUnitOfWork()->detach($data); - Here I assumed that the object would cease to be manageable and would not be updated
But in both cases the state of the original $data changes.
I'm using ApiPlatform 3.0.2
The error is that the main entity is related to an additional entity, so it's not enough to detach the main entity from UnitOfWork. So use the Doctrine\ORM\UnitOfWork->clear(YourEntity::class) method to detach all instances of the entity, and you do the same for relationships.
Once the entity is detach, cloning the entity becomes pointless because the previous entity instance isn't managed by the Doctrine ORM, so my code rearranges itself like this:
public function process($data, Operation $operation, array $uriVariables = [], array $context = [])
{
if (($this->entityCompare)($data)) { // checking for object changes
$this->getEntityManager()->getUnitOfWork()->clear(Entity::class);
$this->getEntityManager()->getUnitOfWork()->clear(EelatedEntity::class);
// do something with new entity
return $this->decorated->process($data, $operation, $uriVariables, $context);
}
return $data;
}

How to share variables to all views (including behavior) in twig?

I have this controller action:
public function index(Request $request)
{
$start = $request->get('start', 0);
$limit = $request->get('limit', 10);
$articles = $this->articleRepository->all($start, $limit);
$navigation = $this->menu->build()->render(new RenderStrategyBootstrap4());
return $this->render('article/index.html.twig', [
'articles' => $articles,
'navigation'=>$navigation
]);
}
I build a menu with:
$navigation = $this->menu->build()->render(new RenderStrategyBootstrap4());
Now this is high level behavior and I do not want to render this for every action there is. Is there a way in Symfony to move this behavior to a sort of view composer (like in Laravel?) and then share the variable with the view?
Or is there no way and do I need to create a base controller?
You could create a Custom Twig Extension as described here: https://symfony.com/doc/current/templating/twig_extension.html
There you can register a custom Twig Function like this:
public function getFunctions()
{
return array('renderNavigation' => new TwigFunction(
'renderNavigation',
array($this, 'renderNavigation'),
array('needs_environment' => true, 'is_safe' => array('html'))
);
}
public function renderNavigation(Environment $twig)
{
/* ... */
return $twig->render(/* ... */);
}
Then you can use the function in every template like {{ renderNavigation() }}
Since the Twig Extension itself is a service you can inject whatever service else you need (like RequestStack, EntityManager and so on) and even cache expensive operations within the extension if you need to function to be run more than once.

SilverStripe extensions for GridFieldConfig

I'm trying to create a class that simplifies re-building a GridFieldConfig each time. I use the following setup for nearly every GridFieldConfig in my CMS:
$config = GridFieldConfig::create()->addComponents(
new GridFieldToolbarHeader(),
new GridFieldAddNewButton('toolbar-header-right'),
new GridFieldTitleHeader(),
... etc
)
Rather than repeating myself each time, I'd rather create a class that returns an instance of GridFieldConfig with the components above. So I created this class, but I'm not sure how to make it work properly and how to plug it into the framework / cms.
<?php
class CustomGridConfig extends ??? {
function __construct() {
$config = GridFieldConfig::create()->addComponents(
new GridFieldToolbarHeader(),
new GridFieldAddNewButton('toolbar-header-right'),
new GridFieldTitleHeader()
... etc
);
return $config;
}
}
Eventually it would be implemented in a GridField as follows:
GridField::create('Foo', 'Bar', $this->Foo(), new CustomGridConfig());
I'm not sure if it's possible to create a class within a class like that, also I'm not quite sure how to get this class loaded into the CMS.
Is the concept of this setup viable? If so, how? This would help me understand how to properly extend the framework / cms.
SilverStripe already comes with a set of GridFieldConfigs out of the box, that may already do what you need.
To answer your question you'd extend GridFieldConfig and do add your components in the constructor like this:
class CustomGridConfig extends GridFieldConfig {
public function __construct() {
$this->addComponents(
new GridFieldToolbarHeader(),
new GridFieldAddNewButton('toolbar-header-right'),
new GridFieldTitleHeader()
... etc
);
}
}
This class will become available to use after you perform a "flush" (appending ?flush to any site URL) - see the documentation on caching.
See the docs for information on the built in configs.
I think you can create custom GridFieldConfigs by extending GridFieldConfig like so:
class CustomGridFieldConfig extends GridFieldConfig {
public function __construct() {
parent::__construct();
$this->addComponent(new GridFieldToolbarHeader());
$this->addComponent(new GridFieldAddNewButton('toolbar-header-right'));
// etc...
}
}
And then pass it to your GridField like so:
GridField::create(
'Foo',
'Bar',
$this->Foo(),
CustomGridFieldConfig::create()
);
Check out the class GridFieldConfig_RelationEditor in file GridFieldConfig.php for inspiration.
your concept is good and setup is viable. I'd have just a plain class and then add your method... should be fine as a constructor but if not a static method should be fine...
class CustomGridConfig {
public static function create() {
$config = GridFieldConfig::create()->addComponents(
GridFieldToolbarHeader::create(),
GridFieldAddNewButton::create('toolbar-header-right'),
GridFieldTitleHeader::create()
... etc
);
return $config;
}
}

Alter service (ClientManager) based on configuration inside bundle extension class

I have a bundle named: "ApiBundle". In this bundle I have the class "ServiceManager", this class is responsible for retrieving a specific Service object. Those Service objects needs to be created based on some configuration, so after this piece of code in my bundle extension class:
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.yml');
// Create Service objects...
I create those Service objects right after I have processed the configuration, something like this:
foreach ($services as $name => $service) {
$service = new Service();
$service->setName($name);
$manager = $container->get($this->getAlias() . '.service_manager');
$manager->add($service);
}
Unfortunately, this does not work, probably because the container isn't compiled yet. So I tried to add those Service objects the following way:
$manager = $container->getDefinition($this->getAlias() . '.service_manager');
$manager->addMethodCall('add', array($service));
But again, this throws the following exception: RuntimeException: Unable to dump a service container if a parameter is an object or a resource.
I can't seem to get a grasp on how to use the service container correctly. Does someone knows how I can add those Service objects to the ServiceManager (which is a service) inside the bundle extension class?
This is how the configuration of the bundle looks like:
api_client:
services:
some_api:
endpoint: http://api.yahoo.com
some_other_api:
endpoint: http://api.google.com
Every 'service' will be a seperate Service object.
I hope I explained it well enough, my apologies if my english is incorrect.
Steffen
EDIT
I think I may have solved the problem, I made a Compiler Pass to manipulate the container there with the following:
public function process(ContainerBuilder $container)
{
$services = $container->getParameter('mango_api.services');
foreach ($services as $name => $service) {
$clientManager = $container->getDefinition('mango_api.client_manager');
$client = new Definition('Mango\Bundle\ApiBundle\Client\Client', array($name, 'client', 'secret'));
$container->setDefinition('mango_api.client.' .$name, $client);
$clientManager->addMethodCall('add', array($client));
}
}
Is this appropriate?
To create services based on configuration you need to create compiler pass and enable it.
Compiler passes give you an opportunity to manipulate other service
definitions that have been registered with the service container.
I think I may have solved the problem, I made a Compiler Pass to manipulate the container there with the following:
public function process(ContainerBuilder $container)
{
$services = $container->getParameter('mango_api.services');
foreach ($services as $name => $service) {
$clientManager = $container->getDefinition('mango_api.client_manager');
$client = new Definition('Mango\Bundle\ApiBundle\Client\Client', array($name, 'client', 'secret'));
$client->setPublic(false);
$container->setDefinition('mango_api.client.' .$name, $client);
$clientManager->addMethodCall('add', array($client));
}
}

Symfony Service setting NULL data

I've been having several issues with creating a service in Symfony 2.0, but have found workarounds for everything but the service setting null data.
public function logAction($request, $entityManager)
{
$logs = new Logs();
$logs->setRoute = $request->get('_route');
$logs->setController = $request->get('_controller');
$logs->setRequest = json_encode($request->attributes->all());
$logs->setPath = $request->server->get('PATH_INFO');
$logs->setIp = $request->server->get('REMOTE_ADDR');
$em = $entityManager;
$em->persist($logs);
$em->flush();
}
I'm passing the EntityManager when calling the service in another controller, I'll post that code just in case:
public function pageAction($id = null, Request $request)
{
$log = $this->get('logging');
$log->logAction($request, $this->getDoctrine()->getEntityManager());
return $this->render('AcmeDemoBundle:Demo:viewPage.html.twig', array('name' => $id));
}
I have created a services.yml file following the official docs (in the Log Bundle) and an actual row is inserted, but all fields are set to NULL. If I try to do a die in one of the setters in the entity file it doesn't die, so it seems like something isn't making it. (I have both the EntityManager and Entity files used at the top of the log service file before anyone asks)
Any help would be greatly appreciated.
$logs->setRoute = $request->get('_route');
Should be:
$logs->setRoute($request->get('_route'));
For D2 all the accessors need to be methods.

Resources