WGR Utvecklingsregler

Detta dokument beskriver regler för hur programmering, databashantering etc ska gå till när vi utvecklar butiksystemet. För system som byggs utanför butiksystemet gäller andra regler enligt nedan. Dokumentet skrivs av Oskar Å. Säg gärna till om något är otydligt eller fattas!

Projekt utanför butiksystemet
För större projekt ska Symfony användas i senaste LTS-version. För mindre projekt, använd Slim med utvalda komponenter från Symfony. Twig används för frontend och PSR som PHP-standard. Namnsätt inte något med Wikinggruppen / WGR i dessa projekt - vi vill låta det vara möjligt för andra firmor att ta över dem. Det finns fler riktlinjer för externa projekt i Wikinggruppens Google Drive.
Exempelprojekt i Symfony: Jaktrapport
Exempelprojekt i Slim: WGR Newsfeed

Generellt

  • Kommentera mycket - med syftet att någon annan person än dig själv enkelt förstår varje del av koden (PHP, JS, CSS - allt!).
  • Tydlighet är viktigare än mikrooptimering.
  • Att hålla konsekvent stil inom ett projekt är viktigare än 100% korrekt stil enligt reglerna i detta dokument.
  • Namnge variabler, funktioner och klasser så att man enkelt förstår innebörden av dem.
  • Alla namnsättningar och kommentarer ska vara på engelska.

Filformat

  • Teckenkodning ska vara UTF-8
    Detta gäller både själva filen samt hur den överförs till klient (rätt charset i HTML och HTTP)

  • Line-endings ska vara LF (unix)
    Obs: din editor kan vara bestämt inställd på en typ av line-breaks, eller så tolkar den filen du öppnar vilka line-breaks det finns flest av i filen.

  • Indentering ska vara tabb (ej mellanslag)
    Se till att byta mellanslag mot tabb om du kopierar kod från t.ex. en guide.

  • Radlängd
    Rader bör vara 80 tecken eller lägre. En maxgräns är 120 tecken som endast får överskridas om det verkligen behövs. Genom att hålla sig under dessa gränser undviker man djupa if-satser och nästlade loopar vilket leder till snyggare kod.

  • Backup
    Om stora delar i en fil ändras, lägg då en kopia av filen i samma mapp fast med tillägget _ÅÅÅÅMMDD.bu i filnamnet (aktuell år-månad-dag). Obs: dessa filer kommer raderas efter ca ett halvår. Exempel: startpage.php_20170907.bu

Allmänt om butiksystemet

Butiksystemet är byggt i PHP i en variant av strukturen Model-View Controller (MVC). Upplägget ser något annorlunda ut i administrationen (kallad backend) och butikens "framsida" (kallad frontend).

Varje kund som använder butiksystemet har en egen kopia av all kod och databas. Då vi har som USP att all kod ska kunna skräddarsys, kan vi sällan automatiskt uppdatera alla kunders kodbaser med nya funktioner och ändringar. Det innebär att koden ser olika ut beroende på vilken version butiksystemet har.

Butiksystemet följer inte alla regler i detta dokument då reglerna arbetats fram efter att butiksystemet byggdes. Ambitionen är dock att reglerna implementeras stegvis i butiksystemet samt att de gäller för all ny utveckling.

För att kunna hantera buggfixar använder vi ett system som kan patcha alla kodbaser (filtrerat på version). Det innebär att vi skickar instruktioner till respektive kodbas för att bland annat göra en "replace". Patchmottagaren söker då efter X i en fil och ersätter med Y. På grund av detta upplägg ger varje differens i filerna en kostnad - att det blir svårare att hålla reda på vad som ska ersättas (X) i en patchning. Detta gäller både ny utveckling samt ändringar i äldre system. Därför är det inte alltid en bra idé att "rätta till" mindre viktiga saker i äldre kodbaser eftersom det då blir svårare att hitta och ersätta via patchningen. Patchverktyget används även så gott det går för att (försöka) lägga in nya funktioner och hjälpklasser.

Butiken har en developer-bar som bara visas för utvalda IP-nummer. Via den kan man aktivera utvecklarläge/produktionsläge. Utvecklarläge innebär att css- och javascriptfiler laddas var för sig med slumptal i sökvägen för att undvika cachening. Produktionsläge innbär att css- och javascriptfiler bakas ihop till färre filer (färre anrop för klienten). Via "Övrigt -> Critical CSS" aktiveras ett läge som visar hur sidan ser ut vid första anropet till servern. Via "Övrigt -> Avaktivera HTML minifiering" stänger du tillfälligt av HTML minifieringen (men bara för din webbläsare).

PHP

Model-View Controller (MVC)

PHP-klasserna ska kategoriseras i olika lager: Model, View, Controller.

  • Controller-klasserna tar hand om ett HTTP-anrop. Den laddar in lämplig Model-klass för att behandla data och ladda det som behövs för att kunna ge ett svar till anropande klient. Controllern bör inte ge något svar till klienten, förutom att sätta cookies, sätta HTTP-status och göra HTTP redirects (Response Headers).

    En Controller kan få ett script att göra olika saker beroende på indata. Till exempel kan den be Model-klassen skapa en produkt, ladda en produkt för redigering eller spara en redigerad produkt. För att avgöra åtgärd ska vi i första hand använda GET-parametern "action" i en switch-sats. Exempel nedan.

  • Model-klasserna bearbetar data. De kan ha kontakt med databas, filer eller externt API. De ska vara tillräckligt fristående för att kunna testas utan HTTP-anrop. De ska inte ge något svar direkt till klienten. Meningen är istället att model-klasserna förbereder data som sedan (eventuellt) skrivs ut genom en View-klass.

  • View-klasserna gör utskrift till klienten (Response Body). HTML ska dock undvikas inom själva klassen. Istället ska HTML läggas i en separat fil som inkluderas av View-klassen. Denna Viewfil fungerar då som en slags template. Den får ha enkel logik såsom if-satser och loopar, men absolut inga databaskommandon, http-anrop eller liknande. All data ska läsas från referenser till Model-klasser.

    Om en del HTML ska återanvändas på olika ställen, t.ex. produktrutorna i ett produktgalleri, kan detta läggas i en separat viewfil i mappen "partials". Obs: För innehåll som loopas ut bör själva loop-satsen köras i en sådan partial-fil så vi slipper köra en include för varje iteration (det blir för kostsamt).

I butiksystemet finns speciella klasser för M, V och C som följer en hierarisk struktur med tre nivåer: Shop, Backend/Frontend och Page. Page-nivån ärver från Backend/Frontend som ärver från Shop. Till exempel har ShopModel tillgång till databasen, vilket därmed görs tillgängligt inom en "PageModel" också.

I backend/butikadmin anropas en php-fil per sida där filen innehåller page-klasser för M,V och C (i vissa fall ligger M i egen fil om den delas av olika sidor i backend).

I frontend sker alla anrop genom en generell FrontendController som tolkar den URL klienten anropar och laddar rätt Page-MVC (i /frontend).

Exempel på fil som innehåller de tre klasstyperna på "page-nivå":

/**
 * Example page-level model class
 */
class WGR_ExamplePageModel extends WGR_BackendModel
{
	/** @var array */
	public $data;

	/**
	 * Loads some example data
	 * @param int $id
	 */
	public function loadData(int $id)
	{
		$standaloneModel = new WGR_StandaloneModel($this);
		$this->data = $standaloneModel->getData($id);
	}
}

/**
 * Example page-level view class
 */
class WGR_ExamplePageView extends WGR_BackendView
{
	/**
	 * Renders the page
	 * @param WGR_ExamplePageModel $pageModel
	 */
	public function render(WGR_ExamplePageModel $pageModel)
	{
		// renderView used from WGR_BackendView
		// template.php will get access to $pageModel
		$this->renderView($pageModel, 'template.php');
	}
}

/**
 * Example page-level controller class
 */
class WGR_ExamplePageController extends WGR_BackendController
{
	/**
	 * Controller method for the page
	 */
	public function execute()
	{
		$pageModel = new WGR_ExamplePageModel();
		$pageView = new WGR_ExamplePageView();

		switch ($pageModel->GET('action')) {
			case 'showTitle':
				$pageModel->loadData((int) $pageModel->GET('id'));
				$pageView->render($pageModel);
				break;
	    }
	}
}

$controller = new ThePageController();
$controller->execute();

Vi ska dock se PageModel-klasserna främst som behållare av data för det som behövs för åtkomst från en PageView. Den mesta databehandlingen ska ligga i en fristående Model-klass som inte ärver tillgång till databasen och liknande (som PageModel-klasser gör). På det viset kan en sådan klass bli användbar från både frontend och backend samt från andra Model-klasser utan problem. Sådana klasser kan liknas vid Services i andra ramverk såsom Symfony. Istället för att ha tillgång till databasen via arv, tar de in en referens till en ShopModel via sin konstruktor. Denna referens ska sparas lokalt i klassen och heta $model. Exempel nedan:

/**
 * Example standalone model
 */
class WGR_StandaloneModel
{
	/** @var WGR_ShopModel */
	private $model;

	/**
	 * @param WGR_ShopModel $model
	 */
	public function __construct(WGR_ShopModel $model)
	{
		$this->model = $model;
	}

	/**
	 * @param int $id
	 * @return array
	 */
	public function getData(int $id)
	{
		return $this->model->dbFetchPrepared(
			'SELECT title_sv, description_sv
			FROM products
			WHERE id = ?',
			array($id),
			PDO::FETCH_ASSOC
		);
	}
}

Hjälpfunktioner

Här listas några av de vanligaste hjälpfunktionerna. En del funktioner finns inte i äldre versioner av butiksystemet - då går det bra att hämta dem från en nyare version.

I klassen WGR_ShopModel finns GET, POST, COOKIE som gör det smidigt att läsa från anropets $_GET etc då de kollar isset och har defaultvärden. Om du ska behandla relativt många värden från ett anrop är det bättre att använda en instans av WGR_ParameterBag vilken finns med sedan v10. Däremot om det handlar om enstaka värden från anropet så bör Controller-klassen läsa värdet och skicka in värdet till Model via funktionsanropet.

I klassen WGR_ShopView finns safeOutput vilken ska användas i viewfiler för att förhindra rendering av html/script injections av data från databasen.

I klassen WGR_DatabaseModel (som ärvs av WGR_ShopModel) finns hjälpfunktioner för databasen samt direktaccess till db-objektet (en PDO-instans). Använd alltid en "prepared" SQL om körningen kräver dynamisk indata, då är det skyddat mot SQL injections.

dbExecuteSimple = SQL som inte ger resultat (osäker)
dbExecutePrepared = SQL som inte ger resultat (säker, skyddar mot SQL injections)
dbFetchSimple, dbFetchPrepared = Hämtar en rad eller null
dbFetchAllSimple, dbFetchAllPrepared = Hämtar alla rader eller null
dbCheckEmptySimple, dbCheckEmptyPrepared = Kollar om en SQL ger 0 st rader
dbBeginTransaction, dbCommitTransaction, dbRollbackTransaction = Används för att hantera transactions i MySQL.

För kontroll och behandling av uppladdade bilder använd WGR_UploadModel. Andra typer av uppladdade filer kan behandlas med hjälp av WGR_FileModel.

Kryptering ska ske med WGR_EncryptionModel men signering och lösenordshantering ska ske av WGR_SignerModel.

För att undvika att script körs av olika processer samtidigt använder vi WGR_LockModel som skapar ett file mutex för att tillåta en process i taget att köra en del av ett script.

Http-anrop ska göras med WGR_HttpClientModel istället för att använda curl direkt.

Objekt/entity

När databasen returnerar flera värden per rad hanterar vi det vanligtvis som stdClass pga parametern PDO::FETCH_OBJ i databasanropet. JSON kan avkodas som antingen array eller objekt. Det är även möjligt att använda klasser, vilka ska läggas i enskilda filer under /shared/model/object. Sådana objektklasser är främst behållare av data men kan innehålla funktioner som behandlar sin egna data, men inte funktioner som beror på andra klasser. Objektklasser ska användas om samma datastruktur ska hanteras av flera klasser. Exempel: WGR_Address i adress.php.

Filstruktur

  • /public_html/butikadmin/
    Filer som anropas direkt av klienten i butikens administrationsdel. Därmed bör de innehålla en Controller-klass men kan även innehålla PageView och PageModel. Dessa klasser ärver alltså från BackendController, BackendView och BackendModel. I vissa fall är PageModel separerad till en egen fil, t.ex. "products_m.php". Detta är inte rätt - det hade varit bättre att använda en fristående Model-klass för produkthanteringen istället för att separera och återanvända en PageModel. Filnamnen ska endast innehålla små bokstäver, siffror och understreck.

  • /public_html/butikadmin/view_desktop/
    Filer vars innehåll utgör response body till klienten, oftast HTML blandat med PHP. Filerna inkluderas från aktuell sidas PageView (i butikadmin). Filnamnen ska endast innehålla små bokstäver, siffror och understreck.

  • /public_html/frontend/
    Filer som körs genom FrontendController. Filerna ska innehålla klasser på Page-nivå, framför allt Model och View. De ska även ha en egen Controller-klass om körningen av modulen/sidan beror på indata från anropet. Filerna ska ha postfix enligt "_m|v|c" som visar vilka typer av klasser de innehåller. Exempelvis "checkoutpage_mvc.php". Filnamnen ska endast innehålla små bokstäver, siffror och understreck.

  • /public_html/frontend/view_desktop/
    Filer vars innehåll utgör response body till klienten, oftast HTML blandat med PHP. Filerna inkluderas från aktuell sidas PageView (i frontend). Filnamnen ska endast innehålla små bokstäver, siffror och understreck.

  • /public_html/shared/model/
    Filer som innehåller fristående Model-klasser, som alltså inte är på Page-nivå utan kan användas som en "service" var som helst. Filnamn ska döpas enligt camelCase, alfanumeriskt, och avslutas med "Model" men ska inte börja med "WGR_". Exempel: emailTemplateModel.php

  • /public_html/shared/model/events/
    Filer med eventlyssnare. Varje modul har en egen fil där eventlyssnare defineras. Filnamn ska döpas enligt modulens namn i camelCase, alfanumeriskt. Exempel: klarnaCheckout.php

  • /public_html/shared/model/object/
    Filer där objektklasser defineras (se ovan om Objekt/Entity).

  • /public_html/shared/model/thirdparty/
    Filer som innehåller fristående Model-klass som främst används för kommunikation med externt API eller SDK. Filnamn ska döpas enligt camelCase, alfanumeriskt, och avslutas med "Model". Exempel: klarnaCheckoutModel.php

  • /public_html/shared/view/
    Filer som innehåller View-klass som inte är på Page-nivå alternativt används av både frontend och butikadmin. Filnamn ska döpas enligt camelCase, alfanumeriskt, och avslutas med "View". Exempel: frontendView.php

  • /public_html/shared/controller/
    Filer som innehåller Controller-klass som inte är på Page-nivå alternativt används av både frontend och butikadmin. Filnamn ska döpas enligt camelCase, alfanumeriskt, och avslutas med "Controller". Exempel: backendController.php

  • /public_html/userfiles/
    Bilder och andra filer som laddats upp via HTML-editorn CKEditor/CKFinder.

  • /var/
    Plats för nyckelfiler, loggfiler, uppladdade filer etc som ej ska vara åtkomliga med webbläsare och aldrig vara intressant att commita till ett Git-repo.

  • Undermappar?
    Om en modul kräver många model-filer eller objekt-filer så är det OK att gruppera dessa i en undermapp döpt efter modulen. Exempel: /shared/model/ticketSystem och /shared/model/object/ticketSystem. Men även i detta fall är det viktigt att klassnamnet innehåller modulnamnet då vi inte använder namespaces för egna klasser. Som vanligt ska filnamnet återspegla modulnamnet trots att de ligger i en dedikerad mapp.

Syntax

  • Kodblock
    Börja varje kodblock med: <?php
    Avsluta inte filen med: ?>

  • Måsvingar
    Funktioner och klasser har måsvingar på egna rader. If-satser, for/while/switch etc (conditional statements) har första måsvingen på samma rad som kommandot. Det ska alltid finnas måsvingar efter if, else och elseif. För else och elseif så ska dessa börja på en egen rad utan måsvinge innan (Stroustrup brace style).

    If-satser som bryter radlängden ska delas upp på flera rader med ett argument per rad, med operatorerna i början av raden. Då sätter vi slutparantesen och måsvingen tillsammans på en egen rad: ) { (se exempel nedan)

  • Strängar
    Använd enkla citationstecken: ' istället för dubbla: "

  • Whitespace
    Lägg ett mellanslag före och efter paranteserna för if/for/while/switch etc (men ej runt parametrarna). Gäller inte funktionsnamn. Mellanslag ska även finnas runt operander förutom ++ och --.
    // Bad
    public function b($c,$j=0)
    {
    	$a=$c;
    	for($i=$j;$i<10;$i+=2){
    		if($i++<5){
    			$a=$i.$a;
    		}
    	}
    }
    
    // Good
    public function b($c, $j = 0)
    {
    	$a = $c;	
    	for ($i = $j; $i < 10; $i += 2) {
    		if ($i++ < 5) {
    			$a = $i . $a;
    		}
    	}
    }

  • If-satser
    Använd elseif istället för else if. Undvik "shorthand if" om inte detta gör kontrollen mer lättläst. Exempel:
    // Good
    $price = $isCampaign ? $campaignPrice : $ordinaryPrice;
    
    // Bad - too complicated!
    $price = ($isCampaign && $customerType == 'private')
    	|| $forceCampaignPrices ?
    		($currency == 'NOK' ? $campaignPriceNOK : $campaignPriceSEK) :
    		($currency == 'SEK' ? $ordinaryPriceNOK : $ordinaryPriceSEK);

  • Kontroll av värden
    Kontroll ska göras med === istället för == om vi känner till datatypen (t.ex. om vi VET att det är en sträng eller en integer, använd ===). Annars är det OK att använda ==. Om det handlar om falseish/trueish, ska jämförelsen inte vara explicit. Tänk bara på att värdet "0" kan vara ett riktigt värde, men kan tolkas som false eller ett tomt databasresultat. Exempel:
    // Good
    if ($action === 'save')
    
    // Bad
    if ($action == 'save')
    
    // Good
    if ($isVisible)
    
    // Bad
    if ($isVisible == 0)
    
    // Good
    if ($cellphone)
    
    // Bad (it could be null)
    if ($cellphone !== '')

  • Tidig return
    En funktion eller if-sats eller for/while-loop kan lätt bli djup om den innehåller nästlade if-satser i flera nivåer. Försök göra "tidig return" istället genom att vända på if-satsen. I en loop blir det alltså en tidig break eller continue enligt samma princip. På detta sätt kan vi även ofta undvika else. Exempel:
    // Bad
    if ($checkOne) {
    	if ($checkTwo) {
    		return true;
    	}
    	else {
    		return false;
    	}
    }
    else {
    	return false;
    }
    
    // Good
    if (!$checkOne) {
    	return false;
    }
    if (!$checkTwo) {
    	return false;
    }
    return true;
    
    // Bad
    foreach ($rows as $row) {
    	if ($row->checkOne) {
    		if ($row->checkTwo) {
    			$sum += $row->amount;
    		}
    	}
    }
    
    // Good
    foreach ($rows as $row) {
    	if (!$row->checkOne) {
    		continue;
    	}
    	if (!$row->checkTwo) {
    		continue;
    	}
    	$sum += $row->amount;
    }

  • Switch-satser
    Break ska indenteras (ett steg från case) och ska finnas i alla case även om det sker en return ovanför. En tom rad ska läggas in mellan varje break och ny case.

  • Arrayer
    Valfritt att använda array(1, 2, 3) eller [1, 2, 3]. Om en array överskrider maxgränsen för rekommenderad radlängd, ange då varje värde på en egen rad, indenterat ett steg från array( med den avslutande parantesen på egen rad i samma nivå som array( eller [. I det fallet ska det sista värdet ha ett komma efter sig, som om det skulle finnas ytterligare en rad.

  • Kompabilitet
    Vi ska se till att PHP-koden fungerar i den senast stabila versionen av PHP (i dagsläget 7.4), detta krav gäller även externa paket från Composer etc. PHP-koden ska dock vara bakåtkompatibel så att den fungerar i en miljö där endast PHP 7.3 finns. Detta eftersom vi gärna återanvänder kod genom guider / moduler och kan smidigt implementera något från en nyare plattform till en äldre. Om vi märker att ett konto kör PHP < 7.3 så ska vi byta till version 7.3 i cPanel.

  • Static
    Undvik statiska klasser och funktioner. Det gör det svårare att byta ut sådana klasser samt att testköra dem. Dessutom blir det knepigt om klassen i framtiden t.ex. behöver tillgång till databasen. Det är dock helt OK att använda statiska variabler inom funktioner.

  • Variabler
    • Använd aldrig global eller $GLOBALS
    • Om ett värde ska användas flera gånger, gör en variabel av det.
    • Det är OK att låta variabler vara public istället för att använda set/get-funktioner.
    • Variabler eller funktioner som endast används inom en class ska vara private.

  • Typer
    Deklarera typer i funktionsargument samt vilken typ en funktion returnerar (om det är en bestämd typ).

  • Kommentarer
    Skriv PHP DocBlocks ovanför varje funktion och klass och i början av varje fil, samt på speciella variabler om det kan hjälpa någon. Lägg inte med ditt namn, editorns namn eller tidsstämpel i dessa DocBlocks.

  • Kedjade anrop
    Om ett kedjat anrop överstiger rekommenderad maxlängd, indentera då varje nytt anrop på egen rad.

  • Externa bibliotek
    Om vi ska använda ett SDK eller annat bibliotek vi inte skrivit själva, använd då Composer (används som standard sedan 9.4.0). I /composer.json anges vilka bibliotek som ska användas i butiksystemet. Dessa ligger i mappen "vendor" utanför public_html med en gemensam autoloader. Vi ska konfigurera composer.json så att den endast hämtar paket som är bakåtkompatibla mot de funktioner vi använder i butiksystemet.

  • Exempel på ovanstående
    /**
     * Includes class definitions for ExampleClass
     */
    
    /**
     * This is an example class
     */
    class WGR_ExampleClass
    {
    	/**
    	 * This is an example function
    	 * @param string $a Possible values: apelsin / banan / citron
    	 * @param int $b Value to be transformed
    	 * @return int Transformed value
    	 */
        public function exampleFunction(int $a, int $b) : int
        {
    	    switch ($a) {
    			case 'apelsin':
    				$b *= 2;
    				break;
    
    			case 'banan':
    				$b += 3;
    				break;
    
    			case 'citron':
    				// No need for further processing, citron is always 5!
    				return 5;
    				break;
    	    }
    
    		// Wide array
    		$fruits = [
    			'apelsin',
    			'banan',
    			'citron',
    		];
    
    	    // Chained commands
    	    $request = \ExternalPaymentProvider\WebPayAdmin::queryOrder($this->config)
    			->setOrderId($transactionId)
    			->setCountryCode($order->shippingAddress->countryCode)
    			->setFruits($fruits)
    			->queryInvoiceOrder();
    
    	    // Long if
            if ($request->getNumberOfApples() > 50
    			&& $request->getNumberOfBananas() > 60
    			&& $request->getNumberOfCoconuts() > 70
    		) {
                return 50;
            }
            elseif ($request->getNumberOfApples() < 30) {
                return $request->getNumberOfDogs();
    		}
    		// No need for "else" here
    		return 1;
        }
    }
    

Autoloading och namespaces

Alla filer som behandlar ett request ska inkludera /environment.php som i sin tur inkluderar /../vendor/autoload.php från Composer. Environment.php aktiverar även errorhantering och ställer in config för databasanslutningen.

Paketen från Composer använder oftast namespaces. I dessa fall anger vi vilka namespaces vi använder med use i klasserna som använder dem.

Innan version 10 har vi historiskt inte haft någon autoloader utan istället använt require_once för att läsa in egna klasser. Det är inte längre nödvändigt tack vare vår egna autoloader i WGR_AutoloadModel, som automatiskt hittar och cachar sökvägar till alla klasser med namn som börjar med "WGR_". Om andra klasser, t.ex. från olika SDK ska användas behöver de inkluderas med require_once om de inte laddas med Composer.

Obs: Det är mycket viktigt att inte lämna några *.php-filer med kopior/backup av klasser eftersom vår autoloader kan välja dessa sökvägar istället för den rätta klassfilen.

Namnsättning

  • Klassnamn ska börja med "WGR_" och fortsätter sedan med CamelCaps. Använd ej namespace förutom om externt SDK kräver det.

  • Funktionsnamn och variabler: camelCase (bör matcha kolumnnamnet i databasen).

  • Config-parametrar: camelCase. Namnet bör börja med modulens namn då alla config-parametrar måste vara unika. Config-parametrar som avgör om en modul ska vara aktiv eller inte ska avslutas med "active". Exempel: sveaCheckoutActive

  • Frasvariabler ska ha engelska namn, endast små bokstäver och understreck, t.ex. search_results. Om en fras ska vara tillgänglig i javascript så måste namnet börja med js_.

  • Konstanter: namnet ska anges i CAPS och understreck, exempel "SORT_TITLE". Använd gärna sträng som värde (exempel "title" till SORT_TITLE) vilket gör det enklare att utöka med fler konstanter senare via guide eller patchning. Med siffervärde finns risk att vi lägger in en ny konstant med samma siffervärde som en tidigare. Exempel på problem: Via ett ärende har vi lagt in SORT_RANDOM = 6, sedan kommer en patch/gitguide som lägger in SORT_POPULAR = 6.

  • Funktioner som gör en viss åtgärd ska ha namn som börjar med...
    • delete om något tas bort (deleteProduct)
    • get om något ska hämtas och returneras (getProduct)
    • load om något ska laddas in till lokal variabel inom klassen (loadProduct)
    • save om något ska sparas (saveProduct)

Komplexitet

Undvik långa funktioner. Om en funktion utför flera olika uppgifter ska den brytas upp den i fler funktioner. Sikta på en maxlängd på 100 rader per funktion. Tänk gärna utifrån ett perspektiv av testning: testbara funktioner måste vara relativt korta och utföra enskilda uppgifter.

Undvik att omsluta stora kodblock i if, else eller try. Var mer specifik istället. Istället för att t.ex. ha en if-sats inuti en for-loop där det händer saker inuti if-satsen, kan en kortare if-sats köra "continue" istället ovanför detta kodblock. På samma sätt kan en hel funktion avbrytas tidigt med en kort if-sats istället för att omsluta hela funktionskroppen av en jättestor if-sats. Try-satser ska alltid vara så snäva som möjligt. Se även avsnittet om "tidig return" under Syntax.

PHP blandat med HTML

  • För utskrift av värden från PHP, använd <?= $variabel ?> med mellanslag före och efter variabeln, utan semikolon.

  • Det är OK att använda echo om utskriften endast gäller PHP-värden, utan någon HTML eller något tecken (paranteser t.ex.) - då ska <?= användas istället så att syntax highlighting fungerar.

  • Om HTML måste skrivas ut inom en klass, bryt ej PHP-blocket. Använd istället echo-kommandon med kommatecken istället för konkatenering.
    echo '<strong>', $title, '</strong>';

  • Börja varje kodblock med <?php och avsluta med ?> och lämna en tom rad på båda ställena (innanför PHP-blocket) om inte brytningen sker vid måsvingar (se exemplet nedan).

  • När PHP-block behöver stängas/öppnas inom en loop eller inom ett conditional statement (if/for/switch osv) så lägg start/slut-taggen på rätt indenteringsnivå - inte i linje med foreach utan ett steg in. I detta läge ska ingen tom rad läggas vid brytningen.

  • Använd inte endif, endforeach och liknande syntax - använd alltid måsvingar.

  • Exempel på ovanstående:
    <div class="outer">
    	<?php
    
    	if ($a) {
    		// Utskrift av HTML i if-sats
    		?>
    		<strong>AAA</strong>
    		<?php
    	}
    	elseif ($b) {
    		// Utskrift av värde och tecken (paranteser)
    		?>
    		(<?= $b ?>)
    		<?php
    	}
    	else {
    		// Utskrift av endast PHP-värde
    		echo $a;
    	}
    
    	?>
    	<div class="inner">
    	     <h1><?= $title ?></h1>
    	     <ul>
    	        <?php
    
    	        foreach ($rows as $row) {
    	            ?>
    	            <li><?= $row->title ?></li>
    				<li><?= $row->author ?></li>
    	            <?php
    	        }
    
    	        ?>
    	     </ul>
    	</div>
    </div>

PHP-version

PHP-koden ska fungera i både PHP-version 7.4 och 7.3.

Utveckling av moduler / tillägg

Ha som mål att modulen ska vara enkel att installera i andra butiker. Därmed ska din kod hållas isolerad/inkapslad och separerad snarare än nästlad med standardkoden, så gott det går. Skapa nya klasser i /shared/model/ (se WGR_StandaloneModel ovan) där den mesta koden läggs, och anropa sedan din class från cartModel istället för att bygga in all din kod i cartModel (exempelvis). Ju mindre vi påverkar standardkoden desto lättare blir den att buggfixa, dessutom blir koden mindre rörig med tiden om vi håller modulerna separerade.

Gör det enkelt att stänga av modulen, genom att lägga till ett configvärde specterIsActive som går att kryssa ur i wgrtools och som blockerar tillgång, cronjob etc till modulen.

Cookies

Om du vill skapa en ny cookie, tänk på att butiksystemet kanske använder en modul för cookiehantering där icke-nödvändiga cookies blockeras automatiskt. Vår egna modul har en kommaseparerad lista i config.cookieControlNecessaryCookies där cookienamnet måste läggas in för att cookien ska godkännas automatiskt.

Cronjob

Cronjob ska om möjligt köras via CLI genom filen /bin/command.php vilket har fördelen att Apache/Cloudflare/etc inte kan bestämma timeout, samt att anropet loggas till Graylog. Se bubbles för exempel på hur scriptet kan användas.

Frontend - generellt

  • Kompabilitetspolicy följer beslut från Visma Product Development Architecture Board (PDAB) men stäms av med vår egna ledningsgrupp samt med större partners såsom Klarna. Sidorna ska fungera i senaste stabila versionerna av Chrome, Firefox, Safari och Edge, i desktop, iOS och Android. Problem som uppstår i andra webbläsare såsom Internet Explorer ska inte hanteras som fel, men kund har rätt att beställa kompabilitetsstöd mot offert.

  • Bilder ska optimeras så långt det går för snabbast möjliga laddning (genom minskad storlek eller progressive). Använd tinypng.com eller motsvarande.

  • Håll HTML, CSS och JS åtskilda. Exempel:
    <!-- Dåligt -->
    <style>
        button {
            color: green;
        }
    </style>
    ...
    <button style="font-weight: bold;" onclick="return submit()">Submit</button>
    ...
    <script>
        function submit() { }
    </script>
    
    <!-- Bra -->
    <!-- style.css -->
    button {
        color: green;
        font-weight: bold;
    }
    
    <!-- index.html -->
    <button id="js-submit">Submit</button>
    
    <!-- jquery.js och script.js -->
    $('#js-submit').on('click', submit);
    
    function submit() { }
    

HTML

  • Doctype
    Använd HTML5 doctypen <!doctype html>

  • HTML-taggar
    Använd alltid små bokstäver.
    Taggar som inte är tomma ska alltid stängas.
    Tomma taggar ska aldrig stängas.
    Attributvärdet ska alltid skrivas innanför dubbla citationstecken (" ")
    <!-- Dåligt -->
    <SPAN>Min andra <br/> text</SPAN>
    <p>Min text
    <img CLASS="img" src="robin.jpg" width=50 height=50 />
    
    <!-- Bra -->
    <span>Min andra <br> text</span>
    <p>Min text</p>
    <img class="img" src="robin.jpg" width="50" height="50">
    
  • Bilder
    Lägg alltid till alt attribut i img-taggar.
    Lägg till width och height för att minimera att sidan hoppar till när bilden laddas in (reflow).

  • Radlängd
    Försök att hålla raderna till max 80 tecken så man inte behöver skrolla i sidleds.

  • <html>,<head> och <body>
    Dessa ska alltid ligga med. <html> ska även ha lang attributet: <html lang="sv">

  • Meta och title
    Meta och title ska alltid skrivas ut före css och js. Se till att en <meta charset="utf-8"> finns med.

  • Kommentarer
    Vid långa kodblock, lägg en HTML-kommentar i slutet för att visa vilken tagg som stängs enligt exemplet nedan.
    <div>
    	<!-- Långt kodblock -->
    </div> <!-- End .l-holder -->
    

  • Link- och scripttaggar
    Link ska ligga inom <head>
    Script ska ligga precis innan </body>
    Link ska inte innehålla type="text/css"
    Script ska inte innehålla type="text/javascript"

  • Attributens ordning
    Använd följande ordning:
    class, id, name, data-*, src, for, type, href, value, title, alt, role, aria-*

  • Tillgång från javascript
    Sätt en class på elementet du vill nå från javascript. Classens namn ska ha prefix js- och är endast till för javascript - classen ska inte ha någon regel i CSS.

  • Boolean attribut
    Deklarera aldrig ett värde i boolean attribut.
    <!-- Dåligt -->
    <input type="checkbox" disabled="disabled" checked="checked">
    
    <!-- Bra -->
    <input type="checkbox" disabled checked>
    

Javascript

  • Variabler
    • Lägg publika/privata variabler i börja av modulen och variabler i funktioner så nära källan som möjligt.
      /**
       * A example module.
       * @module myModule
       */
      
      (function(WGR, $) {
          'use strict';
          
          const publicConst = 'public';
          const privateConst = 'private';
      
          /**
           * My special function.
           * @param {{id: number, isValid: boolean}} options - My special function options.
           * @returns {string}
           */
          function myFunc(options)
          {
              ...
              let myLet = false;
              if (options.isValid) {
                  myLet = true;
              }
              ...
          }
      
          WGR.myModule = {
              publicConst: publicConst,
              myFunc: myFunc
          }
      }(window.WGR = window.WGR || {}, jQuery));
      
    • Föredra let och const före var för att undvika hoisting och för att få tillgång till block scope. Använd const för objekt och värden som inte kommer ändras. Obs: för objekt är det endast pekaren till objektet som blir konstant, inte objektets innehåll.
      // Hoisting
      console.log(variable); // undefined
      var variable = 'My variable';
      
      console.log(letVariable); // ReferenceError
      let letVariable = 'My let variable';
      
      console.log(constVariable); // ReferenceError
      const constVariable = 'My const variable';
      
      // Block scope
      if (true) {
          var variable = 'My variable';
      }
      variable; // My variable
      
      if (true) {
          let letVariable = 'My let variable';
          const constVariable = 'My const variable';
      }
      letVariable; // ReferenceError
      constVariable; // ReferenceError
      
    • Skriv variabler enligt camelCase: myVariable
    • Om ett värde ska användas flera gånger, gör en variabel av det.
    • Deklarera inte globala variabler - Vi har ett globalt object (WGR) använd det. Se även punkten om "Moduler" nedan.
    • // Exempel för global variabel:
      let myGlobalVariable = true; // Fel
      WGR.myGlobalVariable = true; // Rätt
      

  • Måsvingar
    Funktioner har måsvingar på egna rader, förutom om det är en closure-funktion. Objekt, if-satser, for/while/switch etc (conditional statements) har första måsvingen på samma rad som kommandot. Det ska alltid finnas måsvingar efter if, else och else if. För else och else if så ska dessa börja på en egen rad utan måsvinge innan (Stroustrup brace style).

  • Strängar
    Använd enkla citationstecken: ' istället för dubbla: "

  • Whitespace
    Samma regler gäller som för whitespace i PHP.

  • jQuery
    • jQuery ska användas för DOM-manipulation.
    • jQuery kan användas för http-anrop men använd hellre fetch.
    • Använd funktionen $ istället för jQuery.
    • Variabler som är jQuery-objekt ska namnges med $ i början.
    • Använd alltid .on för att lyssna på events (istället för t.ex. .click)
    • Använd alltid .trigger för att köra events (istället för t.ex. .click)

  • Kommentarer
    Skriv JSDoc ovanför varje funktion och i början av varje fil, samt på speciella variabler om det kan hjälpa någon.
    // Examples
    
    /**
     * Fetches a phrase from the compiled phrase file.
     * @param {string} phraseName - Phrase name.
     * @param {string|string[]} [params=[]] - Input data for the phrase.
     * @returns {string}
     */
    function getPhrase(phraseName, params) {
        // [...]
    }
    
    /**
     * Add item to cart.
     * @param {{id: number, name: string}} item - The item to add to cart.
     * @returns {Number}
     */
    function addItem(item) {
        // [...]
    }
    
    /**
     * Find container for area.
     * @param {Object} container - The container to find.
     * @param {number} container.id - The container id.
     * @param {string} container.name - The name of the container.
     * @param {string} container.createdOn - When then container was created.
     * @returns {HTMLElement}
     */
    function findContainerForArea(container) {
        // [...]
    }
  • Moduler
    När vi gör en modul (en specialfunktion som innefattar en eller flera funktioner, variabler etc) ska den göras inkapslad med minimal tillgång utifrån och läggas på det globala WGR-objektet. Följande mall ska användas:
    /**
     * A module that sums numbers.
     * @module specialModule
     */
    
    (function(WGR, $) {
    	'use strict';
    
    	const prefix = 'Total: ';
    
        /**
         * Returns the sum of a and b
         * @param {number} a
         * @param {number} b
         * @returns {number}
         */
    	function add(a, b)
    	{
    		return a + b;
    	}
    
        /**
         * Returns sum of a and b with prefix
         * @param {number} a
         * @param {number} b
         * @returns {string}
         */
    	function getNumber(a, b)
    	{
    		return prefix + add(a, b);
    	}
    
    	WGR.specialModule = {
    		getNumber: getNumber,
    	};
    
    }(window.WGR  = window.WGR || {}, jQuery));
    
    WGR.specialModule.getNumber(2,3); // Total: 5
    
  • Factory functions
    Föredra factory functions framför constructors och class när du ska skapa flera objekt.
    Föredrar:
    • Privata variabler
    • Behöver inte använda this som i vissa fall kan peka på fel objekt
    // Simulate click event
    const element = {
        onClick: function(fn) {
            return fn();
        }
    }
    
    /**
     * Factory
     * @param {string} username
     * @returns {{ getUsername: function }}
     */
    function createUser(username)
    {
        const usernamePrefix = 'Username: ';
    
        return {
            getUsername: function() {
                return usernamePrefix + username;
            }
        }
    }
    
    const user1 = createUser('User1');
    user1.getUsername(); // 'Username: User1'
    element.onClick(user1.getUsername); // 'Username: User1'
    user1.usernamePrefix; // undefined
    
    /**
     * Prototype
     */
    function User(username)
    {
        this.username = username;
        this.usernamePrefix = 'Username: ';
    }
    
    User.prototype.getUsername = function() {
        return this.usernamePrefix + this.username;
    }
    
    const user1 = new User('User1');
    user1.getUsername(); // 'Username: User1'
    element.onClick(user1.getUsername); // NaN
    user1.usernamePrefix; // 'Username: '
    
    /**
     * Class
     */
    class User
    {
        constructor(username) {
            this.username = username;
            this.usernamePrefix = 'Username: ';
        }
    
        getUsername() {
            return this.usernamePrefix + this.username;
        }
    }
    
    const user1 = new User('User1');
    user1.getUsername(); // 'Username: User1'
    element.onClick(user1.getUsername); // NaN
    user1.usernamePrefix; // 'Username: '
    

CSS

Syntax

  • Måsvingar
    Selektorer har första måsvingen på samma rad och avslutande måsvinge på egen rad.

  • Whitespace
    Lägg ett mellanslag mellan varje selektor och dess måsvinge och i varje deklaration mellan kolontecknet och värdet. Använd tabb för att indentera deklarationer och lägg en tom rad mellan CSS blocken.

  • Namnsättning
    Skriv alltid selektorer och deklarationer med små bokstäver och använd bindestreck ( - ) som avgränsare. Tänk på att skriva klasser som beskriver vad dom gör/är inte hur dom ser ut, och använd BEM (se nedan) vid mer komplex kod.
    // Dåligt (namnet är låst till en viss färg)
    .btn--blue {
        background-color: #f00;
    }
    
    // Bra
    .btn--primary {
        background-color: #f00;
    }
    

  • Format
    • Lägg varje selektor på en egen rad vid multi-selektorer (enda undantaget är h1-h6).
    • En deklaration per rad.
    • Använd små bokstäver och "shorthand hex" om det går. Exempel: #000.
    • Använd dubbla citattecken.
    • Vid noll-värden använd 0 eller none. Exempel: margin: 0.
    • Lägg ett mellanslag efter varje kommatecken i en deklaration. Exempel: font-family: "Open sans", sans-serif.
    • Undvik att använda shorthand deklarationer om inte alla värden ska sättas.
    • Sista deklarationen i ett CSS-block ska alltid avslutas med semikolon.
    .selector-1,
    .selector-2,
    .selector-3[type="text"] {
        display: block;
        margin: 0 auto;
    
        background: #fff;
        background: linear-gradient(#fff, rgba(0, 0, 0, 0.8));
    	color: #333;
    	font-family: "Open Sans", arial, sans-serif;    
    }
    
    .selector-a,
    .selector-b {
        padding-top: 10px;
        padding-bottom: 10px;
    }
    

  • Ordning av deklarationer
    Deklarationerna delas upp i tre kategorier (Positioning, Flex, Display & Box Model och Other) och ska alltid listas i den ordningen. Dela upp kategorierna med en radbrytning om dom innehåller mer än en deklaration.
    .selector {
        // Positioning
        position: absolute;
    	bottom: 0;
        left: 0;
        right: 0;
    	top: 0;
    	z-index: 10;
    
    	// Flex
    	display: flex;
    	align-items: center;
    	justify-content: center;
    
        // Display & Box Model
    	border: 10px solid #333;
    	box-sizing: border-box;
        height: 100px;
    	margin: 10px;
    	overflow: hidden;
        padding: 10px;
    	width: 100px;
    
        // Other
        background: #000;
        color: #fff;
        font-family: sans-serif;
        font-size: 1rem;
        text-align: right;
    }
    

  • Kommentarer
    Kommentera alla komponenter. Använd följande struktur:
    //
    // Block
    //
    
    // Basic comment
    

  • Specificitet
    • Använd inte ID i CSS för det ställer till det med specificiteten. Använd klasser istället.
    • Undvik överkvalificerade selektorer. Exempel: ul.nav
    • Använd > (descendant selector) för att begränsa CSS cascadeing
    #holder button {
        border: 1px solid #000;
    }
    
    .form button {
        border: 1px solid #fff;
    }
    
    // Button kommer få en svart border eftersom ID har högre specificitet än en klass.
    
    // Dåligt
    ul.nav {
        display: inline-block;
    }
    
    // Bra
    .nav {
        display: inline-block;
    }
    
    // Undvik
    .nav {
        display: inline-block;
    
        li {
            list-style-type: none;
        }
    }
    
    // Bättre
    .nav {
        display: inline-block;
    
        > li {
            list-style-type: none;
        }
    }
    

  • Pixlar vs em/rem
    Använd alltid pixlar som enhet för media queries. Använd rem som enhet för font-size. Använd relativa enheter för line-height.
    html {
        font-size: 1rem;
        line-height: 1.45;
    }
    

  • Prefix
    Använd prefixet .is- när en klass ska användas både i CSS och Javascript. Klasser med prefixet .js- ska aldrig användas i CSS.
    .btn {
        border: 1px solid #222;
        background-color: #fff;
    }
    
    .is-btn-active {
        background-color: #222;
    }
    

  • Kod från third party
    Om kod från third party använder id och vi inte har möjlighet att ändra på det så är det bättre att använda [id="third-party-widget"].
    // Dåligt (hög specificitet)
    #third-party-widget {
    
    }
    
    // Bättre (låg specificitet)
    [id="third-party-widget"] {
    
    }
    

Struktur

Vi använder oss av följande CSS struktur: Base, Layout, Modules, States, Themes.

  • Base
    Base ändrar utseendet för standard-element på sidan, såsom body, h1.

  • Layout
    Layout delar upp sidan i sektioner och håller moduler på plats. Layout klasser har prefixet .l-

  • Modules
    Moduler utgör största delen av koden och är objekt som kan återanvändas och modifieras beroende på hur dom ska användas.

  • States
    States är klasser som används både i CSS och Javascript. Här är det tillåtet att använda !important. State klasser har prefixet .is-

  • Helpers
    Små hjälpklasser för att förenkla styling av element. Här är det också tillåtet att använda !important, men var sparsam.

// Base
body {
	background-color: #fff;
}

// Layout
.l-holder {
	max-width: 1150px;
}

// Module
.btn {
	font-size: 10px;
}

// Module (modifier)
.btn--large {
    padding: 10px 14px;
    font-size: 20px;
}

// State
.is-hidden {
	display: none !important;
}

// Theme
.btn {
	background-color: #000;
}

Block, element, modifier (BEM)

BEM står för block, element, modifier och är en metod för namnsättning av komponenter på webben. Vi använder BEM för våra CSS moduler (modules).

  • Block
    Block är första nivån av en modul, moduler kan innehålla andra moduler
  • Element
    Element tillhör ett block (modul) och ska därför inte användas utanför modulen. Namnges med dubbla understreck efter blocknamnet.
  • Modifier
    Modifier är en variation av ett block eller element. Namnges med dubbla bindestreck efter block/element-namnet.
// Block (module)
product-item {
    position: relative;
}

// Element
.product-item__heading {
    height: 35px;

    color: #000;
    font-size: 13px;
    line-height: 1.3;
}

// Modifier
.product-item__heading--campaign {
    color: #f00;
}

// HTML
<div class="product-item">
    <h2 class="product-item__heading product-item__heading--campaign">Item 1</h2>
</div>

LESS

LESS är en CSS preprocessor som bygger ut grundfunktionaliteten i CSS med variabler, mixins, nesting och funktioner vilket gör det lättare att underhålla, skapa teman och bygga ut butiken/hemsidan.

  • Nesting:
    Försök att hålla nesting till max tre nivåer.

  • Kodstruktur:
    Kodstrukturen för LESS är följande:
    - modules
    - pages
    - base.less
    - config.less
    - helpers.less
    - includes.less
    - states.less

  • Includes:
    Nya LESS-filer ska inkluderas i includes.less

  • Media queries
    Försök att hålla dina media queries samlade i dina selektorer
    // Dåligt
    .l-holder {
    	position: relative;
    
    	max-width: @page-width;
    	margin-left: auto;
    	margin-right: auto;
    }
    
    ...
    
    @media @page {
    	...
    	.l-holder {
    		margin-left: @page-gutter;
    		margin-right: @page-gutter;
    	}
    }
    
    ...
    
    @media @tablets {
    	...
    	.l-holder {
    		margin-top: 15px;
    	}
    }
    
    // Bra
    .l-holder {
    	position: relative;
    
    	max-width: @page-width;
    	margin-left: auto;
    	margin-right: auto;
    
    	@media @page {
    		margin-left: @page-gutter;
    		margin-right: @page-gutter;
    	}
    
    	@media @tablets {
    		margin-top: 15px;
    	}
    }
    

  • Variabler
    Globala variabler ska ligga i config.less, lokala variabler ska ligga direkt i filen.

Accessibility (Tillgänglighet)

  • Fokus-läge
    Om ni stänger av standard focus (input { outline-style: none; }) se då till att lägga till egen css (input:focus { border-color: blue; } för focus så man tydligt kan tabba mellan element.

  • Kontrast
    Se till att ha hög kontrast mellan text och bakgrund, annars finns det risk att texten flyter ihop med bakgrunden för personer med synfel.

Databas och SQL

PDO

Vi ansluter till MySQL genom PHP-paketet PDO. Använd PDO's egna konstanter för att välja resultatyp. Oftast används PDO::FETCH_OBJ eller PDO::FETCH_COLUMN.

Återanvändbara statements

Ska samma SQL-sats köras flera gånger i en loop med olika indata per iteration? Använd då ett "prepared statement". Databasen tolkar & kompilerar då frågan 1 gång men exekverar den flera gånger. Exempel:

$dbStatement = $this->db->prepare(
	'INSERT INTO foo (bar)
	VALUES (?)'
);

while (1) {
	$dbStatement->execute(array('hej'));
}

Rådata och säkerhet

Vi ska spara rådata i databasen (genom "prepared statements") istället för att t.ex. byta ut citationstecken och script-taggar innan. Risken finns då att någon sparar ett javascript i databasen som körs när värdet skrivs ut. Scriptet kan då t.ex. kopiera en administratörs cookie och skicka iväg den. Därför är det viktigt att vid utskrift alltid använda t.ex. hjälpfunktionen safeOutput som gör om text till html-entities.

Även om vi vet att indata till en SQL är riktiga integers så ska vi använda prepare-funktionerna då det dels blir en vanesak att använda säkra metoder, dels bättre syntax highlighting i editorerna när SQL-strängen inte behöver brytas. Simple-funktionerna bör därmed bara användas om SQL-satsen inte har någon dynamisk indata.

Prestanda

  • Skrivoperationer = kostsamt
    Uppdatera bara databasen om det behövs. Exempel: vi har ett script som regelbundet läser produktdata från externt håll och ska uppdatera butiken. Uppdatera då bara databasen om något faktiskt ändrats. Antingen genom att hämta ut nuvarande data innan, eller använda WHERE för att undvika uppdatering av rader där t.ex. priset redan är som det ska.

  • Färre anrop
    Undvik i största mån att köra en SELECT-fråga per iteration i en loop. Gör istället en fråga innan loopen och hämta sedan från det resultatet per iteration.

SQL-syntax

Skriv SQL med varje "SQL-ord" i versaler. Låt varje logiskt avsnitt (FROM/JOIN/WHERE/ORDER/GROUP/LIMIT/VALUES etc) börja på egen rad. Om raderna trots det blir långa, bryt raden och fortsätt indenterat. Följ detta även för korta SQL-satser.

Om det hämtas värden från flera olika tabeller, gruppera då värdena efter tabell och gärna med en radbrytning emellan grupperna.

Undvik SELECT * eftersom det underlättar PHP-skrivande om man vet vilka kolumner som finns tillgängliga. Undantag är dynamiska kolumner som beror på t.ex. antal aktiva språk.

Lägg ett space före paranteserna i INSERT/VALUES. Exempel:

SELECT a.name, a.hej, a.lussebulle,
	b.pris, b.cykel,
	c.skiva, c.armborst, c.lampa
FROM apelsin AS a
INNER JOIN banan AS b ON b.id = a.bID
INNER JOIN (
	SELECT skiva, armborst, lampa
	FROM citron
	ORDER BY RND()
	LIMIT 1
) AS c ON c.type = a.cType

SELECT id
FROM apelsin
WHERE bID = 2

DELETE a.*
FROM apelsin AS a
LEFT JOIN banan AS b ON b.id = a.bID
WHERE b.pris IS NULL

INSERT INTO apelsin (hej, du)
VALUES ('tjena', 'tja')

SQL i PHP

  • Börja strängen som innehåller SQL på en ny rad, indenterat (alternativt bygg upp strängen i en variabel i förväg).
  • Gör radbrytningar och indenteringar för att underlätta läsbarheten, enligt exemplet nedan.
  • Använd ? som placeholder för PDO. Om det är fler än 5 placeholders, överväg att använda namngivna placeholders för att göra det mer läsbart.
Exempel:

$dbResult = $this->dbFetchAllPrepared(
	'SELECT a.id
	FROM apelsin AS a
	WHERE a.banan = ?',
	array($banan),
	PDO::FETCH_OBJ
);

$dbResult = $this->dbFetchAllPrepared(
	'SELECT a.id
	FROM apelsin AS a
	WHERE a.banan = ?
		AND (a.citron = ? OR a.citron = ? OR a.citron = ?)
		AND a.damask = ?',
	array(
		$banan,
		$citron1,
		$citron2,
		$citron3,
		$damask,
	),
	PDO::FETCH_OBJ
);

$this->dbExecutePrepared(
	'INSERT INTO people (firstName, lastName, streetRow1, streetRow2, zipCode, city)
	VALUES (:firstName, :lastName, :streetRow1, :streetRow2, :zipCode, :city)'
	array(
		':firstName' => $firstName,
		':lastName' => $lastName,
		':streetRow1' => $streetRow1,
		':streetRow2' => $streetRow2,
		':zipCode' => $zipCode,
		':city' => $city,
	)
);

Databasdesign

  • Vi använder för närvarande MySQL-version 5.7

  • Lagringsmotor ska vara: InnoDB

  • Radformat ska vara: DYNAMIC

  • Kollationering ska vara: utf8mb4_swedish_ci (det är OK att använda andra om vi vet att inga konstiga tecken eller åäö ska sparas)

  • Teckenkodning ska vara: utf8mb4. Obs: utf8mb4 har en maxlängd på 191 tecken om det ska användas som ett index (primary/unique). Om det måste vara längre än så, använd utf8 istället - men då fungerar inte emojis och annat som kräver 4 bytes/tecken!

  • Fältnamn och tabellnamn ska vara på engelska, i camelCase.

  • Fältnamn för språk ska avslutas med understreck och språkkod: camelCase_sv

  • Tabellnamn för relationer mellan olika tabeller: product_categories

  • För ID i relationstabeller: använd "productID" istället för "product".

  • Använd samma namn på samma sak i olika tabeller! Inte t.ex. metaKeys på ett ställe och metaKeywords på ett annat ställe.

  • Använd samma fältnamn som variabelnamn i PHP (så de kan laddas direkt in som objekt i PHP).

  • Boolean: I databasen används en TINYINT för inställningar som är på/av eller sant/falskt. Värdet 0 används för "false" och 1 för "true". När detta läses av PHP är det fortfarande 0 och 1. Så jämför därför inte strikt mot boolean. Namnge dessa variabler i formen av en frågeställning, t.ex. isHidden, isDefault. Exempel:
    $isHidden = $this->dbFetchSimple(
    	'SELECT isHidden
    	FROM config',
    	PDO::FETCH_COLUMN
    );
    
    if (!$isHidden) {
    	// Show the item
    }

  • Använda en så liten datatyp som möjligt för respektive värde. Exempelvis räcker en unsigned TINYINT som ID för land. En MEDIUMINT kan räcka till 16777215 unika värden - det räcker till mycket och sparar 1 byte per inlägg jämfört med INT. Obs: det är inte lagringsutrymmet som är problemet utan själva bandbredden i minnet. Referens: https://dev.mysql.com/doc/refman/5.7/en/integer-types.html

  • Om en kopia tas av en tabell som backup, ska den döpas enligt: tabellnamn_ÅÅÅÅMMDD_bu.

Databasoptimering

Det är viktigt att ha grundläggande koll på index och bra/dåliga sätt att skriva SQL-frågor då den vanligaste flaskhalsen är MySQL som får arbeta i onödan.

Index
Skapa index för allt som kan användas för sökning, sortering eller som relationsvärde till andra tabeller. Index bör byggas med flera kolumner anpassade efter SQL som körs mot tabellen. Om det går att ha flera kolumner i indexet är det ofta bättre än flera mindre index. Ordningen på kolumnerna bör vara:

  1. Krav/filter
  2. Sortering/gruppering
  3. Datavärden
Inbördes sortering av ovanstående bör göras i ordning av "cardinality". Ett exempel är att foreignID bör vara före resourseType i URL-tabellen eftersom foreignID har högre cardinality = fler unika värden, vilket gör att mysql snabbare får ett snävare resultat att bläddra i.

Datavärden i indexet kräver mer lagringsutrymme och ska inte användas i onödan, men fördelen är att mysql inte behöver göra en extra läsning av data om värdet redan finns i själva indexet.

Exempel på fix av ihopsatt index i URLs:
- från: resourceType, foreignID, isOld, languageID
- till: foreignID, productLastCategory, languageID, resourceType, isOld

Exempel på fix av ihopsatt index i products_defaultCategory:
- från: productID, frontID, categoryID
- till: productID, categoryID, frontID

Men det är viktigt att komma ihåg att index bara används om dess kolumner kan användas från vänster till höger. Så ett index med (productID, categoryID, frontID) används inte om frågan inte kollar mot productID utan bara categoryID.

Funktioner
Index kan inte användas om värdena går genom funktioner i SQL. Exempel på urval för ordrar de 2 senaste dagarna:
# Bad
WHERE DATEDIFF(NOW(), orderTime) <= 2

# Good
WHERE orderTime >= DATE_SUB(NOW(), INTERVAL 2 DAY)

Join vs sub-select
Tänk på att varje JOIN kan expandera resultatet som mysql processar med alla rader i joinen. Det leder ofta till att mysql måste skapa en temporär tabell istället för att hålla det i minnet. Det kan gå snabbare att använda sub-select för att hålla antalet tempoära rader nere samt att bättre utnyttja index i de olika faserna av exekveringen. Exempel nedan där EXISTS gör det 10x snabbare eftersom uppslaget avbryts direkt vid matchning.
# Bad
SELECT fi.id, fi.filterID, fi.title_sv AS title,
	fi.imageFilename, GROUP_CONCAT(fp.productID) AS productIDs
FROM filter_items AS fi
INNER JOIN filter_products AS fp ON fp.filterItemID = fi.id
INNER JOIN products_categories AS pc ON pc.productID = fp.productID
INNER JOIN products AS p ON (p.id = fp.productID AND p.isHidden = 0)
WHERE pc.categoryID = 47
GROUP BY fi.id
ORDER BY fi.listOrder

# Good
SELECT fi.id, fi.filterID, fi.title_sv AS title, fi.imageFilename,
	(
        SELECT GROUP_CONCAT(productID)
     	FROM filter_products
     	WHERE filterItemID = fi.id
    ) AS productIDs
FROM filter_items AS fi
WHERE EXISTS (
	SELECT 1
    FROM products_categories AS pc
    INNER JOIN products AS p ON p.id = pc.productID AND p.isHidden = 0
    INNER JOIN filter_products AS fp ON fp.productID = p.id
    WHERE pc.categoryID = 47 AND fp.filterItemID = fi.id
)
ORDER BY fi.listOrder

Analysera
Använd EXPLAIN SELECT för att se hur din fråga exekveras. När SELECT körs genom wgrtools visar vi automatiskt resultatet från EXPLAIN. Då går det att upptäcka onödiga sökningar och expansioner av resultat, samt vilka index som används.

Git

Detta gäller de butiker och/eller ärenden som använder Git. Se dokumentet "Git och Gitlab" i Wikinggruppens Google Drive för mer information om Git, Gitlab och lokal utveckling.

  • Varje ärende ska ha sin egna branch med namn task-ärendeId

  • Pusha din branch tidigt till Gitlab och skapa ett Merge Request (MR) av den. Namnsätt ditt MR med "WIP:" (work in progress) tills du är klar att merga eller granskas. Namnet på MR bör även kort förklara ärendet (dvs ungefär samma som ärendets namn i wgrsecure). Länka till wgrsecure-ärendet från MR. Genom att skapa ett MR tidigt gör du det möjligt för andra att följa arbetet och ge råd längs vägen.

  • Databasändringar ska läggas i en ny SQL-fil med datum och ärende-id i filnamnet: /database/migrations/yyyymmdd-task-ärendeId.sql

  • Commitmeddelande ska vara kortfattat och på engelska, i formen "denna commit kommer att...": Add logging for newsletter signup

  • Bunta inte ihop flera olika ändringar i samma commit - de ska kunna förklaras enskilt i varsitt commitmeddelande.

Changelog

  • 2021-11-22 - OskarÅ
    • Information om cronjob och http-anrop
  • 2021-05-19 - OskarÅ
    • Information om godkända cookies
    • Information om fetch
  • 2021-04-15 - OskarÅ
    • Ny information om vår egna autoloader för PHP
    • Uppdaterad info om kompabilitetspolicy för webbläsare
  • 2020-08-07 - OskarÅ
    • Översyn på allt!
  • 2019-04-01 - OskarÅ
    • Mer info om whitespace i PHP och JS
  • 2019-03-04 - OskarÅ
    • Tar bort krav om stöd för PHP 5.4
    • Info om långa if-satser som bryter radlängden
    • Mer info om databassäkerhet och prepared/simple-funktionerna
    • Mer info om projekt utanför butiksystemet
    • Tar bort info om RequestData
    • Ändrad info om partials
    • Info om struktur för nya moduler
    • JS: Info om arrow functions
    • JS: Mer info om const och let
  • 2018-11-29 - Robin
    • Byt ut kraken.io till tinypng.com
    • undefined i moduler behövs inte längre för den är readonly i nyare webbläsare (IE 10 >)
    • Använd use strict i moduler
    • Lägg till stöd för let och const
    • Lägg till factory functions
    • Förtydliga vart man ska deklarera js variabler
    • Använd JSDoc för att kommentara javascript
  • 2018-09-04 - OskarÅ
    • Info om backup av databastabeller
    • Info om PHP 7.1
  • 2018-08-03 - Robin
    • Ändra utseende på LESS-kommentarer
    • Förtydliga css states och helpers
    • Förklara hur vi använder LESS
  • 2018-06-04 - Robin
    • Ändra lägsta gräns från IE 9 till IE 11
  • 2018-04-10 - OskarÅ
    • Fler Docblocks i PHP-exemplen
    • Syntaxfel i PHP-exemplen rättade
  • 2018-04-04 - OskarÅ
    • Förtydliganden i PHP blandat med HTML
    • Ändrad info om hur echo tillåts
  • 2018-02-28 - OskarÅ
    • Radformat för InnoDB ska vara DYNAMIC
  • 2018-02-27 - OskarÅ
    • Tydligare att dokumentet beskriver butiksystemet.
    • Ny info och exempel på Model-klass som inte är page-model.
    • Bryter ut namnsättning till egen sektion.
    • Namnsättning för config-parametrar.
    • Namnsättning för fraser.
    • Namnsättning för funktioner med vanliga syften.
    • Ny sektion om Komplexitet.
    • Tydligare info om måsvingar för else/elseif.
    • Info om stora arrayer.
    • Info om att undvika statiska funktioner.
    • Info om externa bibliotek och Composer.
    • Info om dbGetLock.
  • 2017-09-18 - OskarÅ + Robin
    • Första versionen