I'm confused how to project models result in view in joomla 2.5
I have the controller which is initializing the model
class FrontpageMyComponentControllerItem extends JControllerLegacy
{
private $id;
public function display($cachable = false, $urlparams = array())
{
// Initialise variables.
$jinput = JFactory::getApplication()->input;
$this->id = $jinput->get('id');
$cachable = true;
$model = $this->getModel('item');
$result_in_view = $model->Item('23'); //$id what I get
// Set the default view name and format from the Request.
$viewName = $jinput->get('view', 'item');
$jinput->set('view', $viewName);
return parent::display($cachable, $safeurlparams);
}
}
now how do I have the result in my view?
In most components data (results) are being retrieved from Model by the View.
I'm not really sure why, but I guess it's to give more power to Views.
FrontpageMyComponentViewItem extends JViewLegacy
{
/** #var array Data are stored here */
public $items;
public function display($tpl = null)
{
/** Retrieve data from Model */
$items = $this->get('Items');
// Check for errors.
if (count($errors = $this->get('Errors')))
{
// Raise an error
JError::raiseWarning(500, implode("\n", $errors));
return false;
}
// Assign data, so layout can access these
$this->items =& $items;
parent::display($tpl);
}
}
This is different than usual MVC implementations where Controller is retrieving data from Model and passing it to View.
If you'd like to see example of this in Joomla, look at MediaController in Joomla 2.5.
Sometimes the View may not be necessary (output is in JSON) and then you may retrieve data inside controller and output it instantly using echo like in search suggestions
There are new MVC (vs legacy) classes in Joomla, but core components still don't use them.
Related
I want to switch my code to an async implementation. When I want to do this then I notice that my related data gets not set automatically after I retrieve them like it used to do it.
This is the initial function that gets called from an API controller. I used the AddDbContext function to add the dbcontext class via dependency injection into my controller:
public async Task<Application> GetApplicationById(AntragDBNoInheritanceContext dbContext, int id)
{
List<Application> ApplicationList = await dbContext.Applications.FromSqlRaw("Exec dbo.GetApplication {0}", id).ToListAsync();
Application Application = ApplicationList.First();
if(Application != null)
{
await CategoryFunctions.GetCategoryByApplicationID(Application.Id);
}
}
The GetCategoryByApplicationId function loads the related category of an application which is a many to one relation between Category and Application:
public async Task<Category> GetCategoryByApplicationID(int applicationID)
{
var optionsBuilder = new DbContextOptionsBuilder<AntragDBNoInheritanceContext>();
optionsBuilder.UseSqlServer(ApplicationDBConnection.APPLICATION_CONNECTION);
using (var dbContext = new AntragDBNoInheritanceContext(optionsBuilder.Options))
{
List<Category> category = await dbContext.Categories.FromSqlRaw("Exec GetApplicationCategory {0}", applicationID).ToListAsync();
if (category.Any())
{
return category.First();
}
}
return null;
}
When I want to retrieve an application then the field Category is not set. When I did not use async/await it would set the category automatically for me. Of course I could just return the Category Object from the GetCategoryByApplicationId and then say:
Application.Category = RetrievedFromDbCategory;
But this seems a bit unmaintainable compared to the previous behaviour. Why does this happen now and can I do something about it? Otherwise I don't see much benefits on using async/await .
I've implemented a Dynamic Serialization group via Context Builder for admin users (adding admin:write). And have assigned this group to the property I want only updatable by an admin via GraphQL.
My implementation at this point is taken directly from https://api-platform.com/docs/core/graphql/#changing-the-serialization-context-dynamically
But when attempting to mutate this property I am given an error that reads Field "roles" is not defined by type updateUserInput.
This makes some sense to me as the schema does not contain this property since it is not in the typical write group. However, the documentation suggests this should be doable. If this is the case, what am I not doing correctly?
Relevant Code:
Context Builder
<?php
namespace App\Serializer;
use ApiPlatform\Core\GraphQl\Serializer\SerializerContextBuilderInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
/**
* Context Builder: Experimental implementation used for constructing what resources are returned.
*/
final class AdminGroupsContextBuilder implements SerializerContextBuilderInterface {
private $decorated;
private $authorizationChecker;
/**
*
*/
public function __construct(SerializerContextBuilderInterface $decorated, AuthorizationCheckerInterface $authorizationChecker) {
$this->decorated = $decorated;
$this->authorizationChecker = $authorizationChecker;
}
/**
*
*/
public function create(?string $resourceClass, string $operationName, array $resolverContext, bool $normalization): array {
$context = $this->decorated->create($resourceClass, $operationName, $resolverContext, $normalization);
$resourceClass = $context['resource_class'] ?? NULL;
if (isset($context['groups']) && $this->authorizationChecker->isGranted('ROLE_ADMIN') && FALSE === $normalization) {
$context['groups'][] = 'admin:input';
}
return $context;
}
}
User Entity Class property definition
/**
* #ORM\Column(type="json")
* #Groups({"read", "admin:write"})
*/
private $roles = [];
Services Definition
App\Serializer\AdminGroupsContextBuilder:
decorates: 'api_platform.graphql.serializer.context_builder'
arguments: [ '#App\Serializer\AdminGroupsContextBuilder.inner' ]
autoconfigure: false
I have a REST API. I need to create presentation (DTO) object, but the construction of this object depends on request - it differs in 15%.
I wonder what pattern should I use.
My case:
//presentation-DTO
class Item {
private $name;
private $price;
private $tags;
private $liked; //is Liked by logged user
...
public function __construct(Item $item, bool $liked, ...)
{
$this->name = $item->getName();
$this->price = $item->getPrice();
$this->tags = $item->getTags();
$this->liked = $liked;
...
}
}
When user is not logged in - I don't need $liked
When showing list of items - I don't need $tags
And there are more attributes that works as above.
My first idea was to use Builder principle.
$itemBuilder = new ItemBuilder();
$itemBuilder->setItem($item);
...
if($user) {
$itemBuilder->setUserLiked($userLiked);
...
}
return $itemBuilder->build();
It solves my problem with too many parameters in constructor.
But still, I also don't need all parameters to be constructed - eg. I don't need tags (on lists). As I use lazy load, I don't want my dto constructor to call them.
So I thought, maybe Factory.. but then my problem with too many (and optional) parameters is returning.
How will you solve this?
Sorry I don't have required points to make a comment hence an answer.
What are you trying to do with the Item class. Your class is Item and first parameter is also of type Item. I cannot visualizes how its going to work.
I will prefer to keep business login to set proper properties in a separate class:
/**
* A class for business logic to set the proper properties
*/
class ItemProperties {
private $item;
public $isLogin = false;
public $showList = false;
.....
public function __construct(Item &$item) {
// set all properties;
}
public function getProperties() {
$retVal = [];
if($this->isLogin == true) {
$retVal['liked'] = true;
}
if($this->showList == true) {
$retVal['tags'] = $this->item->getTags();
}
if(....) {
$retVal['...'] = $this->item->.....();
}
return $retVal;
}
}
/**
* DTO
*/
class Item {
public function __construct(ItemProperties $itemProps) {
$this->setItemProps($itemProps);
}
// If you prefer lazy loading here...maybe make it public
// and remove call from constructor.
private function setItemProps(&$itemProps) {
$properties = $itemProps->getProperties();
foreach($properties AS $propName => $propValue) {
$this->$propName = $propValue;
}
}
}
// Usage:
$itemProps = new ItemProperties($Item);
// set other properties if you need to...
$itemProps->isLogin = false;
$item = new Item($itemProps);
On ASP.NET 5 a Component view must be in one of two places:
Views/NameOfControllerUsingComponent/Components/ComponentName/Default.cshtml
Views/Shared/Components/ComponentName/Default.cshtml
Is there a way to change this to:
Views/NameOfControllerUsingComponent/Components/ComponentName.cshtml
Views/Shared/Components/ComponentName.cshtml
So basically, remove the folder ComponentName and change the view name from Default.cshtml to ComponentName.cshtml.
For me it makes more sense ... Is it possible?
That convention is only applied if you create a view component that derives from the base ViewComponent provided by the framework.
That class defines the View helpers, which return a ViewViewComponentResult:
public ViewViewComponentResult View<TModel>(string viewName, TModel model)
{
var viewData = new ViewDataDictionary<TModel>(ViewData, model);
return new ViewViewComponentResult
{
ViewEngine = ViewEngine,
ViewName = viewName,
ViewData = viewData
};
}
The ViewViewComponentResult is where the conventions are defined:
private const string ViewPathFormat = "Components/{0}/{1}";
private const string DefaultViewName = "Default";
public async Task ExecuteAsync(ViewComponentContext context)
{
...
string qualifiedViewName;
if (!isNullOrEmptyViewName &&
(ViewName[0] == '~' || ViewName[0] == '/'))
{
// View name that was passed in is already a rooted path, the view engine will handle this.
qualifiedViewName = ViewName;
}
else
{
// This will produce a string like:
//
// Components/Cart/Default
//
// The view engine will combine this with other path info to search paths like:
//
// Views/Shared/Components/Cart/Default.cshtml
// Views/Home/Components/Cart/Default.cshtml
// Areas/Blog/Views/Shared/Components/Cart/Default.cshtml
//
// This supports a controller or area providing an override for component views.
var viewName = isNullOrEmptyViewName ? DefaultViewName : ViewName;
qualifiedViewName = string.Format(
CultureInfo.InvariantCulture,
ViewPathFormat,
context.ViewComponentDescriptor.ShortName,
viewName);
}
...
}
Notice that if you return from your view component the full path to a view as the view name, then the view component will use the specified view.
Something like:
return View("~/Views/Shared/Components/ComponentName.cshtml")
Since there is no way to modify the conventions in ViewViewComponentResult and your approach would only work for view components with a single view, you could build something using the root view paths approach:
Create your own ViewComponent class extending the existing one.
Add new helper methods or hide the existing View methods to return a view using a full path:
public ViewViewComponentResult MyView<TModel>(TModel model)
{
var viewName = string.Format(
"~/Views/Shared/Components/{0}.cshtml",
this.ViewComponentContext.ViewComponentDescriptor.ShortName)
return View(viewName, model);
}
If you add new methods you might be able to add them as extension methods of ViewComponent instead of having to create your own class.
Another alternative would be creating a class SingleViewViewComponent copying the code for ViewComponent but replacing the implementation of ViewViewComponentResult View<TModel>(string viewName, TModel model). Then when creating your view components, you would inherit from SingleViewViewComponent instead of ViewComponent.
Took me a weekend to finally find a way around this that didn't involve writing a custom ViewComponentResult.
in MVC .Net Core, you can add your own IViewLocationExpander to the RazorViewEngineOptions in your startup.cs's ConfigureServices:
public void ConfigureServices(IServiceCollection services)
{
...
services.Configure<RazorViewEngineOptions>(options =>
{
options.ViewLocationExpanders.Add(new CustomLocationExpander());
});
}
This allows you to add custom Paths that are used in conjuction with the ViewLocationExpanderContext ViewName ({0}), ControllerName ({1}).
The main issue is that you can't alter the context's values, which makes it seemingly impossible to change the default View Component's ViewName of Component/ComponentName/Default
Seemingly impossible
Here's the trick, the ExpandViewLocations is called with each View(), each time it doesn't have a fully qualified view path. Which means you can add custom logic. What I did was add a catch to detect ViewComponents in the PopulateValues method, then added to the context.Values dictionary, and then if that dictionary has those custom values, it will prepend to the Paths the list of paths that use my generated view name instead of the context.
It's fully reverse compatible, and shouldn't impact performance one bit.
public class CustomLocationExpander : IViewLocationExpander
{
private const string _CustomViewPath = "CustomViewPath";
private const string _CustomController = "CustomController";
public void PopulateValues(ViewLocationExpanderContext context)
{
Regex DefaultComponentDetector = new Regex(#"^((?:[Cc]omponents))+\/+([\w\.]+)\/+(.*)");
/*
* If successful,
* Group 0 = FullMatch (ex "Components/MyComponent/Default")
* Group 1 = Components (ex "Component")
* Group 2 = Component Name (ex "MyComponent")
* Group 3 = View Name (ex "Default")
* */
var DefaultComponentMatch = DefaultComponentDetector.Match(context.ViewName);
if (DefaultComponentMatch.Success)
{
// Will render Components/ComponentName as the new view name
context.Values.Add(_CustomViewPath, string.Format("{0}/{1}", DefaultComponentMatch.Groups[1].Value, DefaultComponentMatch.Groups[2].Value));
context.Values.Add(_CustomController, context.ControllerName);
}
}
public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
{
/* Parameters:
* {2} - Area Name
* {1} - Controller Name
* {0} - View Name
* */
List<string> Paths = new List<string> {
// Default View Locations to support imported / legacy paths
"/Views/{1}/{0}.cshtml",
"/Views/Shared/{0}.cshtml",
// Adds Feature Folder Rendering
"/Features/{1}/{0}.cshtml",
"/Features/Shared/{0}.cshtml",
// Handles My Custom rendered views
"/{0}.cshtml"
};
// Add "Hard Coded" custom view paths to checks, along with the normal default view paths for backward compatibility
if (context.Values.ContainsKey(_CustomViewPath))
{
// Generate full View Paths with my custom View Name and Controller Name
var CombinedPaths = new List<string>(Paths.Select(x => string.Format(x, context.Values[_CustomViewPath], context.Values[_CustomController], "")));
// Add in original paths for backward compatibility
CombinedPaths.AddRange(Paths);
return CombinedPaths;
}
// Returns the normal view paths
return Paths;
}
}
My system is working fine for small database and my report is generates from at least 5 table of phpmyadmin after certain limit of data load '500 internal server error' will come.I want enhance exporting a report to csv/excel from phpmyadmin using yii for larger database.
I use this extension to export to CSV. http://www.yiiframework.com/extension/csvexport/
I have created an action that I can attach to any controller that I need to export.
<?php
class Csv extends CAction {
public $field_list;
public function run() {
$controller = $this->getController();
/* Disable the logging because it should not run on this function */
foreach (\Yii::app()->log->routes as $route) {
if ($route instanceof \CWebLogRoute) {
$route->enabled = false;
}
}
\Yii::import('core.extensions.ECSVExport.ECSVExport');
//use the existing filters
$model_name = $controller->modelName();
$model = new $model_name('search');
$dataProvider = $model->search();
$criteria = $dataProvider->criteria;
//remove the pagination
$dataProvider->setPagination(false);
//changing the criteria to only select what we want
$criteria->select = implode(',', $this->field_list);
$dataProvider->setCriteria($criteria);
//export to CSV
$csv = new \ECSVExport($dataProvider);
if(isset($_GET['test'])) {
echo $csv->toCSV();
} else {
\Yii::app()->getRequest()->sendFile($controller->modelName() . '_'.date('Y-m-d').'.csv', $csv->toCSV(), "text/csv", false);
exit();
}
}
}
field_list are the fields that I need to export.
For the controller I add
/**
* #return array actions to be mapped to this controller
*/
public function actions(){
return array(
'csv'=>array(
'class'=>'core.components.actions.Csv',
'field_list'=>array('t.id', 't.name', 't.status'),
),
);
}
/**
I use the same search as in the controller because it suits me and because I use http://www.yiiframework.com/extension/remember-filters-gridview/ I actually can export exactly what is on the screen. Change the field list to what you need. Remember to give access to the csvAction function.
You can use yiiexcell extension for that: http://www.yiiframework.com/extension/yiiexcel/ it is simple wrapper for PHPExcel http://phpexcel.codeplex.com/releases/view/107442