File structure
customerService.PHP
include 'vo/VOCustomer.php';
include 'mydb.php';
class customerService
{
public function createCustomer(VOCustomer $cus)
{
$db = new mydb();
$db->connect();
$query = sprintf("insert into customer (CusId, CusName, CusContact,idcompany) values ('%s','%s','%s','%s')",
mysql_real_escape_string($cus->CusId),
mysql_real_escape_string($cus->CusName),
mysql_real_escape_string($cus->CusContact),
mysql_real_escape_string($cus->idcompany));
$rs = mysql_query($query) or die ("Unable to complete query.");
return 'success';
}
}
vo/VOCustomer.php
class VOCustomer {
public $CusId;
public $CusName;
public $CusContact;
public $idcompany;
}
When importing the customerService.php to a flex zend project Its possible that the data type may not return as VOCustomer sometimes it will show Object as type
How to make the passing object as VOcustomer object ?
I'm not sure the 'Connect to PHP' wizard understands type hinting.
Even if it does Zend AMF will pass an Objet not a VOCustomer to the method.
It's safer to add a PHPDoc comment:
/**
* #param VOCustomer $cus
*/
public function createCustomer($cus)
Second add dummy function to your service that returns VOCustomer. The 'Connect to PHP' wizard generates a value object only if it's returned by a service method.
/**
* #return VOCustomer
*/
public function getCustomer() {
//Do nothing
}
Related
I use DTOs as the data_class for Symfony form types. There is one thing that does not work for me when I use typed properties (PHP 7.4) in these DTOs.
EXAMPLE:
class ProductDto
{
/*
* #Assert\NotBlank
*/
public string $title;
}
This generally seems to work quite well – in case the user submits the form with a blank title or description, the validation kicks in and the form is displayed with validation warnings.
BUT THERE IS A PROBLEM when data is added while creating a form (e.g. the edit form):
$productDto = new ProductDto();
$productDto->title = 'Foo';
$form = $this->createForm(ProductFormType::class, $productDto);
Initially the form is displayed as expected with Foo as the value for the title. When a user clears the title input form field and submits the form an exception like this is thrown:
Typed property `App\Form\Dto\ProductDto::$title` must be string, null used
As far as I can see this is caused by the fact that during Form->handleRequest() the title is set to null after it was set to "Foo" before, right?
Is there a solution for this problem?
Since PHP 7.4 introduces type-hinting for properties, it is particularly important to provide valid values for all properties, so that all properties have values that match their declared types.
A property that has never been assigned doesn't have a null value, but it is on an undefined state, which will never match any declared type. undefined !== null.
Here is an example:
<?php
class Foo
{
private int $id;
private ?string $val;
public function __construct(int $id)
{
$this->id = $id;
}
}
For the code above, if you did:
<?php
foo = new Foo(1);
$foo->getVal();
You would get:
Fatal error: Uncaught Error: Typed property Foo::$val must not be
accessed before initialization
See this post for more details https://stackoverflow.com/a/59265626/3794075
and see this bug https://bugs.php.net/bug.php?id=79620
This is what I just came up with:
DTO:
use GenericSetterTrait;
/**
* #Assert\NotBlank
*/
public string $title;
public function setTitle(?string $title): void
{
$this->set('title', $title);
}
/**
* #Assert\NotNull
*/
public Foo $foo;
public function setFoo(?Foo $foo): void
{
$this->set('foo', $foo);
}
Trait:
trait GenericSetterTrait
{
private function set(string $propertyName, $value): void
{
if ($value === null) {
unset($this->{$propertyName});
} else {
$this->{$propertyName} = $value;
}
}
}
Seems to work. What do you think? Any objections?
I'm currently building a web application, and went for Symfony 4 along with API Platform.
I built a custom data provider in order to pull data from a XML file, for an entity. Since it's all one-way operations, I only enabled GET operations for both items and collections.
I am trying to tie the entity served by this custom data provider to a Doctrine entity, but I'm getting an error saying that the entity is not a valid one or mapped super class.
How do I create a relationship between these two? Is it even possible?
Thanks!
This is a snippet from the aforementioned entity:
<?php
//src/Entity/Sst.php
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Annotation\ApiProperty;
/**
* #ApiResource(
* collectionOperations={"get"={"method"="GET"}},
* itemOperations={"get"={"method"="GET"}}
* )
*/
class Sst
{
public $code_urssaf;
/**
* #ApiProperty(identifier=true)
*/
public $code_sst;
// ...and a few others
public function getCodeUrssaf(): ?string
{
return $this->code_urssaf;
}
public function setCodeUrssaf(string $code_urssaf): self
{
$this->code_urssaf = $code_urssaf;
return $this;
}
public function getCodeSst(): ?string
{
return $this->code_sst;
}
public function setCodeSst(string $code_sst): self
{
$this->code_sst = $code_sst;
return $this;
}
// and so on; this is generated then tuned with Symfony's MakerBundle.
Here's a bit from the collection data provider, with imports omitted (but everything works when querying the API directly):
final class SstCollectionDataProvider implements CollectionDataProviderInterface, RestrictedDataProviderInterface
{
public function __construct(FilesystemInterface $extdataStorage, SerializerInterface $serializer)
{
$this->serializer = $serializer;
$this->storage = $extdataStorage;
}
public function supports(string $resourceClass, string $operationName = null, array $context = [] ): bool
{
return Sst::class === $resourceClass;
}
public function getCollection(string $resourceClass, string $operationName = null): \Generator
{
$sstfile = $this->storage->read('SST_29072019.xml');
$sstlist = $this->serializer->deserialize($sstfile, SstCollection::class, 'xml', array('object_to_populate' => $sstlist = new SstCollection()));
foreach($sstlist as $sstObject)
{
yield $sstObject;
}
}
}
The Doctrine entity has this, mirroring other relationships:
/**
* #var Sst[]
*
* #ORM\ManyToOne(targetEntity="App\Entity\Sst")
*/
private $sst;
I expect to be able to tie the custom entity to the Doctrine one, but I cannot even start the Symfony app, I'm getting:
In MappingException.php line 346:
Class "App\Entity\Sst" is not a valid entity or mapped super class.
I have 2 services, BlueWorkerService and YellowWorkerService, both implementing the same interface, WorkerServiceInterface. Each of these services use the same entities but with different required logic.
I need to inject one of, but not both, of these classes and use them in ProcessorService so that the interface methods are called using on correct Worker. Which worker service to use is dependent on which Worker is currently being processed. I'll break it down:
Class WorkerProcessor {
private $workerService;
public function __construct(WorkerServiceInterface $workerServiceInterface)
{
$this->workerService = $workerServiceInterface;
}
public function getMixedColourWithRed() {
return $this->workerService->mixWithRed();
}
}
The worker service that is being used would be based on whether the worker being processed has the colour property of Blue or Yellow.
I know I can probably use a Factory to achieve this as described here but my problem is how to tell the factory which Worker colour I am processing?
Running on Symfony 3.4
If you need more info, just ask and I will update the question.
NOTE: I'm using Symfony 4.3.1. I'll post it like that, then I'll help you to move all code from this architecture to Symfony 3.4.
I'm using a similar concept to load different classes in my project. Let me explain first, then I'll add code under this text.
Firstly, I'm loading a custom compiler pass under src/Kernel.php (your file is app/AppKernel.php):
/**
* {#inheritDoc}
*/
public function build(ContainerBuilder $container)
{
$container->addCompilerPass(new BannerManagerPass());
}
BannerManagerPass its created under src/DependencyInjection/Compiler (in your case should be src/BUNDLE/DependencyInjection/Compiler`).
class BannerManagerPass implements CompilerPassInterface
{
/**
* {#inheritDoc}
*/
public function process(ContainerBuilder $container)
{
if (!$container->has(BannerManager::class)) {
return;
}
$definition = $container->findDefinition(BannerManager::class);
$taggedServices = $container->findTaggedServiceIds('banner.process_banners');
foreach (array_keys($taggedServices) as $id) {
$definition->addMethodCall('addBannerType', [new Reference($id)]);
}
}
}
As you see, this class should implement CompilerPassInterface. You can observe that I'm looking for specific services tagged as banner.process_banners. I'll show how I tagged services a little bit later. Then, I'm calling addBannerType method from BannerManager.
App\Service\BannerManager.php: (in your case src/BUNDLE/Service/BannerManager.php)
class BannerManager
{
/**
* #var array
*/
private $bannerTypes = [];
/**
* #param BannerInterface $banner
*/
public function addBannerType(BannerInterface $banner)
{
$this->bannerTypes[$banner->getType()] = $banner;
}
/**
* #param string $type
*
* #return BannerInterface|null
*/
public function getBannerType(string $type)
{
if (!array_key_exists($type, $this->bannerTypes)) {
return null;
}
return $this->bannerTypes[$type];
}
/**
* Process request and return banner.
*
* #param string $type
* #param Server $server
* #param Request $request
*
* #return Response
*/
public function process(string $type, Server $server, Request $request)
{
return $this->getBannerType($type)->process($request, $server);
}
}
This class has a custom method (created by me) called process(). You can name it whatever you want it, but I think that's pretty verbose. All parameters are sent by me, so don't mind. You can send whatever you want.
Now we have our Manager and compiler pass is set. It's time to set our banner types (based on my example) and tag them!
My banner types are under src/Service/Banner/Types (in your case should be src/BUNDLE/Service/WhateverYouWant/Type. This does not matter! You can change it later from services.yaml).
These types are implementing my BannerInterface. It does not matter the code under the class in this instance. One more thing that I should warn you! You should see that under BannerManager, inside the addBannerType() I'm calling $banner->getType(). This is one method inherited from BannerInterface in my case and it has a unique string (in my example I have three banner types: small, normal, large). This method can have any name, but don't forget to update it as well in your manager.
We are almost ready! We should tag them, then we are ready to try them!
Go to your services.yaml and add these lines:
App\Service\Banner\Types\:
resource: '../src/Service/Banner/Types/'
tags: [banner.process_banners]
Please see the tag!
Whatever I want to show a custom banner, I'm using a simple URL with $_GET where I keep my banner type, then I load it like this:
public function view(?Server $server, Request $request, BannerManager $bannerManager)
{
...
return $bannerManager->getBannerType($request->query->get('slug'))->process($request, $server);
}
first of all I am trying to use services for the first time... (Actually if s.o. could give a short info about how and when and why to use it.. nice ;-) )
But now to my specific case:
I wrote two controllers:
One for uploading a xlsx file to the server
One for importing the xlsx data to the DB
What I now want to do is to pass the (uploaded)path from the uploading controller to the import controller. am I correct to use the import as a service?
Code looks as the following...
class FileUploadController extends Controller
/**
* #Route("/upload", name="upload")
* #Security("has_role('ROLE_ADMIN')")
*/
public function uploadAction(Request $request){
$companyid = $this->getUser()->getCompany();
if ($request->getMethod() == 'POST'){
$file = $request->files->get('xls');
$uploadedURL = '';
if(($file instanceof UploadedFile) && $file->getError()=='0'){
if(!($file->getSize()<20000)){
$originalName = $file->getClientOriginalName();
$name_array = explode('.',$originalName );
$file_type = $name_array[(sizeof($name_array)-1)];
$valid_filetypes = array('xls', 'xlsx');
if(in_array(strtolower($file_type), $valid_filetypes)){
$document = new Document();
$document->setFile($file);
$document->setSubDirectory('uploads');
$document->processFile();
$uploadedURL=$uploadedURL=$document->getUploadDirectory().DIRECTORY_SEPARATOR.$document->getSubDirectory().DIRECTORY_SEPARATOR.$file->getBasename();
}else{
echo "Wrong File Ending";
}
}else {
echo "File to big";
}
}else{
print_r('File Error');
die;;
}
$this->get("dataimport.service")->importIndexAction($uploadedURL);
}else{
return $this->render(bla)
DataImportController as:
class DataImportController extends Controller
/**
* #param ContainerInterface $container
*/
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
/**
* #Security("has_role('ROLE_ADMIN')")
*/
public function importIndexAction($path)
{
$companyid = $this->getUser()->getCompany();
$em = $this->getDoctrine()->getManager();
$file = $this->defineFilePathAction($path);
$reader = $this->readExcelAction($file);
$accountarray = $this->getAccountsArrayAction($companyid);
$this->importAccountsAction($companyid, $reader, $accountarray, $em);
}
....
/**
* Get a service from the container
*
* #param string The service to get
*/
public function get($service)
{
return $this->container->get($service);
}
services.yml
services:
dataimport.service:
class: AppBundle\Controller\DataHandling\DataImportController
arguments: [#service_container]
Thanks for your help!
In Symfony a controller and service is at first just a class. A controller's public method is meant to take an input and generates an Response (output) (by the way injecting the Request is deprecated, you have to use the current request from the request_stack). A service is an object out of the DI container with no constraints at all.
Since the controller's method has to generate and return a response, it's mostly not a good idea to invoke a controller from another controller, because you maybe don't need that response, but only the implementation of the method.
That's also the reason why you should move reusable code to services. A controller should actually only:
extract data from the request
call some services
render a template with the result
Same for Commands. The services are the core of your application. Horizontal communication between controllers or commands is mostly a bad idea (only of course some proxies or wrapper).
Here are some ideas for your code:
The action itself is too much unreadable code. If you get the uploaded file via symfony form read this https://stackoverflow.com/a/28754907/4469738
Don't access the request directly if you use forms. The reason is, that only your builder or Type class which creates the form (and the data class), knows the name of the input fields and maps them to a data class. You should just use the data class. Then you get a nice UploadedFile object to check everything, but also move the checks to services.
I researched the How to Handle File Uploads with Doctrine and I don't want to hard-code the __DIR__.'/../../../../web/'.$this->getUploadDir(); path because maybe in future I will change the web/ directory. How to do it more flexible? I found this but it doesn't answer the question how to do it more flexible from inside the Entity
You shouldn't use entity class as a form model here. It's simply not suitable for that job. If the entity has the path property, the only valid values it can stores are: null (in case lack of the file) and string representing the path to the file.
Create a separate class, that's gonna be a model for your form:
class MyFormModel {
/** #Assert\File */
private $file;
/** #Assert\Valid */
private $entity;
// constructor, getters, setters, other methods
}
In your form handler (separate object configured through DIC; recommended) or the controller:
...
if ($form->isValid()) {
/** #var \Symfony\Component\HttpFoundation\File\UploadedFile */
$file = $form->getData()->getFile();
/** #var \Your\Entity\Class */
$entity = $form->getData()->getEntity();
// move the file
// $path = '/path/to/the/moved/file';
$entity->setPath($path);
$someEntityManager->persist($entity);
return ...;
}
...
Inside form handler/controller you can access any dependencies/properties from DIC (including path to the upload directory).
The tutorial you've linked works, but it's an example of bad design. The entities should not be aware of file upload.
To access the root directory from outside the controller you can simply inject '%kernel.root_dir%' as an argument in your services configuration.
service_name:
class: Namespace\Bundle\etc
arguments: ['%kernel.root_dir%']
Then you can get the web root in the class constructor:
public function __construct($rootDir)
{
$this->webRoot = realpath($rootDir . '/../web');
}
You can use a variable in your parameters.yml.
Like this you'll can change path when you want.
for example :
# app/config/parameters.yml
# Upload directories
upload_avatar_dir: /uploads/avatars
upload_content_dir: /uploads/content
upload_product_offer_dir: /uploads/product-offer
...
I handled this by creating an abstract class that Entities may extend if they are handling file uploads as described in the Symfony Documentation. I created the files array so I could create a copy of the existing file path in the set methods so it could be deleted off the file system on a successful update or delete without defining any additional properties in the Entity proper.
use Symfony\Component\HttpFoundation\File\File;
abstract class FileUploadEntity
{
private $files;
public function __set($name, File $value)
{
$this->files[$name] = $value;
}
public function __get($name)
{
if (!is_array($this->files)) $this->files = array();
if (!array_key_exists($name, $this->files)) {
return null;
}
return $this->files[$name];
}
public function getUploadRootDirectory()
{
return $this->getWebDirectory() . $this->getUploadDirectory();
}
public function getWebDirectory()
{
return __DIR__ . "/../../../../web/";
}
public function getUploadDirectory()
{
$year = date("Y");
$month= date("m");
return "images/uploads/$year/$month/";
}
public function getEncodedFilename($name)
{
return sha1($name . uniqid(mt_rand(), true));
}
// this should be a PrePersist method
abstract public function processImages();
// This should be defined as a Doctrine PreUpdate Method
abstract public function checkImages();
// this should be a PostPersist method
abstract public function upload();
// this should be a PostUpdate method and delete old files
abstract public function checkUpload();
// This should be a PostRemove method and delete files
abstract public function deleteFile();
}